乐闻世界logo
搜索文章和话题

面试题手册

Maven 的 POM 文件有哪些核心元素?如何配置 POM 文件?

Maven 的 POM(Project Object Model)文件是 Maven 项目的核心配置文件,使用 XML 格式描述项目的各种信息。POM 文件位于项目根目录,文件名为 pom.xml。POM 文件的基本结构:<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- 项目基本信息 --> <groupId>com.example</groupId> <artifactId>my-project</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <!-- 项目描述 --> <name>My Project</name> <description>Project description</description> <url>https://example.com</url> <!-- 属性配置 --> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <spring.version>5.3.20</spring.version> </properties> <!-- 依赖管理 --> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> </dependencies> <!-- 依赖版本管理 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>${spring.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- 构建配置 --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> </plugin> </plugins> </build></project>POM 文件的核心元素:项目坐标(Coordinates):groupId:项目组 ID,通常是公司或组织域名倒序artifactId:项目构件 ID,通常是项目名称version:项目版本号,遵循语义化版本规范packaging:打包类型,如 jar、war、pom、ear 等项目信息:name:项目显示名称description:项目描述url:项目主页 URLlicenses:项目许可证developers:开发者信息scm:源代码管理信息依赖管理:dependencies:项目依赖列表dependencyManagement:依赖版本管理exclusions:依赖排除构建配置:build:构建配置plugins:插件配置resources:资源配置filters:资源过滤器其他配置:properties:属性定义profiles:配置文件repositories:仓库配置parent:父 POM 引用POM 继承:子项目可以通过 <parent> 元素继承父 POM 的配置:<parent> <groupId>com.example</groupId> <artifactId>parent-project</artifactId> <version>1.0.0</version> <relativePath>../pom.xml</relativePath></parent>最佳实践:使用属性(properties)统一管理版本号在父 POM 中使用 dependencyManagement 管理依赖版本保持 POM 文件简洁,避免过度配置使用注释说明复杂的配置遵循 Maven 的项目结构约定定期更新依赖版本,保持项目安全性POM 文件的继承顺序:父 POM 的配置当前 POM 的配置激活的 Profile 配置命令行参数配置理解 POM 文件的结构和配置对于有效使用 Maven 至关重要。
阅读 0·2月18日 21:28

Maven 的多模块项目如何管理?聚合和继承有什么区别?

Maven 的多模块项目(Multi-Module Project)通过聚合(Aggregation)和继承(Inheritance)机制来管理复杂的项目结构。这两种机制可以单独使用,也可以结合使用。1. 聚合(Aggregation):聚合用于将多个模块组合在一起构建,通过在父 POM 中使用 <modules> 标签声明子模块。当构建父项目时,Maven 会自动构建所有子模块。<project> <groupId>com.example</groupId> <artifactId>parent-project</artifactId> <packaging>pom</packaging> <modules> <module>module-a</module> <module>module-b</module> <module>module-c</module> </modules></project>2. 继承(Inheritance):继承用于在子模块中共享父 POM 的配置,通过在子 POM 中使用 <parent> 标签引用父 POM。子模块可以继承父 POM 的依赖管理、插件配置、属性等。<project> <parent> <groupId>com.example</groupId> <artifactId>parent-project</artifactId> <version>1.0.0</version> </parent> <artifactId>module-a</artifactId></project>父 POM 的常用配置:<dependencyManagement>:统一管理依赖版本,子模块只需声明 groupId 和 artifactId<pluginManagement>:统一管理插件配置<properties>:定义公共属性,如版本号、编码格式等<dependencies>:定义所有子模块共享的依赖<build>:定义公共的构建配置多模块项目的优势:代码组织清晰,按功能或层次划分模块依赖管理统一,避免版本冲突构建效率高,可以单独构建特定模块便于团队协作,不同团队负责不同模块支持并行构建,提高构建速度最佳实践:父 POM 的 packaging 必须是 pom子模块之间通过依赖关系引用,避免循环依赖使用相对路径引用子模块,便于项目迁移在父 POM 中使用 <dependencyManagement> 统一管理版本合理划分模块粒度,避免模块过多或过少常用命令:mvn clean install:构建整个项目mvn clean install -pl module-a:只构建 module-amvn clean install -pl module-a -am:构建 module-a 及其依赖的模块mvn clean install -pl '!module-a':构建除 module-a 外的所有模块
阅读 0·2月18日 21:27

如何确保 SQLite 数据库的安全性?

SQLite 的安全性需要从多个层面考虑:数据库加密使用 SQLCipher 扩展进行数据库加密支持多种加密算法(AES-256 等)加密对性能有一定影响 -- 使用 SQLCipher 加密数据库 PRAGMA key = 'encryption_key';访问控制SQLite 本身不支持用户权限管理需要在应用层实现访问控制使用文件系统权限控制数据库文件访问在网络环境中使用中间件层进行权限管理SQL 注入防护使用参数化查询(Prepared Statements)避免字符串拼接构建 SQL 语句 # Python 示例:使用参数化查询 cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))输入验证验证所有用户输入限制输入长度和格式使用白名单而非黑名单验证数据完整性使用约束确保数据完整性启用外键约束:PRAGMA foreign_keys = ON;定期备份数据库安全配置 -- 限制危险操作 PRAGMA secure_delete = ON; -- 删除数据时覆盖 -- 禁用加载扩展 PRAGMA load_extension = OFF;传输安全如果数据库文件在网络传输,使用加密传输避免在不可信网络上传输明文数据库使用 VPN 或加密隧道审计和监控记录数据库访问日志监控异常查询模式定期审查数据库访问权限移动应用安全不要将数据库存储在可访问的位置使用设备加密功能考虑使用钥匙串或密钥库存储加密密钥最佳实践最小权限原则定期更新 SQLite 版本进行安全代码审查实施数据备份和恢复计划SQLite 的安全性主要依赖于应用层实现,开发者需要全面考虑各个安全层面。
阅读 0·2月18日 21:27

如何优化 SQLite 数据库的性能?

SQLite 的性能优化需要从多个方面考虑:数据库设计优化合理设计表结构,避免过度规范化使用适当的数据类型,减少存储空间为频繁查询的列创建索引使用 WITHOUT ROWID 表减少存储开销查询优化使用 EXPLAIN QUERY PLAN 分析查询执行计划避免使用 SELECT *,只查询需要的列使用索引列进行 WHERE、JOIN、ORDER BY、GROUP BY 操作避免在索引列上使用函数或表达式使用 LIMIT 限制返回结果数量事务优化将多个操作包装在一个事务中,减少磁盘 I/O使用 WAL 模式提高并发性能保持事务简短,减少锁持有时间使用适当的隔离模式(DEFERRED、IMMEDIATE、EXCLUSIVE)连接和语句优化使用预编译语句(prepared statements)提高性能复用数据库连接,避免频繁打开关闭批量插入数据时使用事务使用参数化查询防止 SQL 注入并提高性能配置优化 -- 启用 WAL 模式 PRAGMA journal_mode = WAL; -- 设置同步模式(FULL、NORMAL、OFF) PRAGMA synchronous = NORMAL; -- 设置缓存大小(页数) PRAGMA cache_size = -20000; -- 20MB -- 设置临时存储为内存 PRAGMA temp_store = MEMORY; -- 设置页面大小 PRAGMA page_size = 4096;定期维护使用 VACUUM 重建数据库,回收空间使用 ANALYZE 更新统计信息,帮助查询优化器使用 REINDEX 重建碎片化的索引定期备份数据库应用层优化使用连接池管理数据库连接实现查询缓存机制异步执行耗时操作合理使用内存数据库(:memory:)处理临时数据通过综合应用这些优化技巧,可以显著提高 SQLite 数据库的性能。
阅读 0·2月18日 21:27

TypeORM 如何支持多种数据库?包括 MySQL、PostgreSQL、MongoDB 等的配置和使用

TypeORM 支持多种数据库类型,包括 MySQL、PostgreSQL、SQLite、SQL Server、Oracle 和 MongoDB。本文将详细介绍 TypeORM 如何配置和使用多个数据库。支持的数据库类型关系型数据库MySQL / MariaDBimport { DataSource } from 'typeorm';const mysqlDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});PostgreSQLconst postgresDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});SQLiteconst sqliteDataSource = new DataSource({ type: 'sqlite', database: './myapp.db', entities: [User, Post], synchronize: false, logging: true,});SQL Serverconst mssqlDataSource = new DataSource({ type: 'mssql', host: 'localhost', port: 1433, username: 'sa', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});Oracleconst oracleDataSource = new DataSource({ type: 'oracle', host: 'localhost', port: 1521, username: 'system', password: 'password', serviceName: 'ORCL', entities: [User, Post], synchronize: false, logging: true,});NoSQL 数据库MongoDBconst mongoDataSource = new DataSource({ type: 'mongodb', host: 'localhost', port: 27017, database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});多数据库配置配置多个 DataSourceimport { DataSource } from 'typeorm';// 主数据库 (MySQL)const primaryDataSource = new DataSource({ type: 'mysql', name: 'primary', // 数据源名称 host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'primary_db', entities: [User, Post], synchronize: false, logging: true,});// 从数据库 (PostgreSQL)const secondaryDataSource = new DataSource({ type: 'postgres', name: 'secondary', // 数据源名称 host: 'localhost', port: 5432, username: 'postgres', password: 'password', database: 'secondary_db', entities: [Comment, Like], synchronize: false, logging: true,});// 缓存数据库 (Redis)const cacheDataSource = new DataSource({ type: 'mongodb', name: 'cache', host: 'localhost', port: 27017, database: 'cache_db', entities: [Cache], synchronize: false, logging: true,});初始化多个数据源async function initializeDataSources() { try { // 初始化所有数据源 await Promise.all([ primaryDataSource.initialize(), secondaryDataSource.initialize(), cacheDataSource.initialize(), ]); console.log('All data sources initialized successfully'); } catch (error) { console.error('Error initializing data sources:', error); throw error; }}// 使用示例initializeDataSources().then(() => { // 应用程序逻辑});跨数据库操作在不同数据库间操作// 从 MySQL 读取用户async function getUserFromPrimary(userId: number) { const userRepository = primaryDataSource.getRepository(User); return await userRepository.findOne({ where: { id: userId } });}// 从 PostgreSQL 读取评论async function getCommentsFromSecondary(postId: number) { const commentRepository = secondaryDataSource.getRepository(Comment); return await commentRepository.find({ where: { postId } });}// 写入 MongoDB 缓存async function cacheUserData(userId: number, data: any) { const cacheRepository = cacheDataSource.getRepository(Cache); const cache = cacheRepository.create({ key: `user:${userId}`, value: JSON.stringify(data), expiresAt: new Date(Date.now() + 3600000), // 1小时后过期 }); return await cacheRepository.save(cache);}// 组合使用async function getUserWithCache(userId: number) { // 先尝试从缓存读取 const cacheRepository = cacheDataSource.getRepository(Cache); const cached = await cacheRepository.findOne({ where: { key: `user:${userId}` } }); if (cached && cached.expiresAt > new Date()) { return JSON.parse(cached.value); } // 从主数据库读取 const user = await getUserFromPrimary(userId); // 写入缓存 await cacheUserData(userId, user); return user;}跨数据库事务// 使用分布式事务模式async function transferDataBetweenDatabases( userId: number, postId: number) { const primaryQueryRunner = primaryDataSource.createQueryRunner(); const secondaryQueryRunner = secondaryDataSource.createQueryRunner(); try { // 开始事务 await primaryQueryRunner.connect(); await primaryQueryRunner.startTransaction(); await secondaryQueryRunner.connect(); await secondaryQueryRunner.startTransaction(); // 在主数据库操作 const userRepository = primaryQueryRunner.manager.getRepository(User); const user = await userRepository.findOne({ where: { id: userId } }); // 在从数据库操作 const commentRepository = secondaryQueryRunner.manager.getRepository(Comment); const comment = commentRepository.create({ userId, postId, content: 'Comment from user', }); await commentRepository.save(comment); // 提交事务 await primaryQueryRunner.commitTransaction(); await secondaryQueryRunner.commitTransaction(); } catch (error) { // 回滚事务 await primaryQueryRunner.rollbackTransaction(); await secondaryQueryRunner.rollbackTransaction(); throw error; } finally { // 释放资源 await primaryQueryRunner.release(); await secondaryQueryRunner.release(); }}数据库特定配置MySQL 特定配置const mysqlDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User], synchronize: false, logging: true, // MySQL 特定配置 extra: { connectionLimit: 10, // 连接池大小 acquireTimeout: 60000, // 获取连接超时时间 timeout: 60000, // 查询超时时间 waitForConnections: true, // 等待可用连接 queueLimit: 0, // 队列限制 charset: 'utf8mb4', // 字符集 timezone: '+00:00', // 时区 ssl: { rejectUnauthorized: false } }, // MySQL 驱动选项 driverOptions: { flags: ['+FOUND_ROWS'], namedPlaceholders: true, },});PostgreSQL 特定配置const postgresDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'password', database: 'myapp', entities: [User], synchronize: false, logging: true, // PostgreSQL 特定配置 extra: { max: 10, // 最大连接数 idleTimeoutMillis: 30000, // 空闲连接超时 connectionTimeoutMillis: 2000, // 连接超时 }, // PostgreSQL 驱动选项 driverOptions: { application_name: 'MyApp', statement_timeout: 10000, query_timeout: 10000, },});MongoDB 特定配置const mongoDataSource = new DataSource({ type: 'mongodb', host: 'localhost', port: 27017, database: 'myapp', entities: [User], synchronize: false, logging: true, // MongoDB 特定配置 url: 'mongodb://localhost:27017/myapp', useNewUrlParser: true, useUnifiedTopology: true, // MongoDB 驱动选项 driverOptions: { maxPoolSize: 10, minPoolSize: 2, maxIdleTimeMS: 60000, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, },});数据库迁移管理多数据库迁移// 主数据库迁移export class CreateUserTable1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable( new Table({ name: 'user', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'name', type: 'varchar', }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('user'); }}// 从数据库迁移export class CreateCommentTable1234567890124 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable( new Table({ name: 'comment', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'content', type: 'text', }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('comment'); }}运行特定数据库的迁移// 运行主数据库迁移async function runPrimaryMigrations() { await primaryDataSource.runMigrations();}// 运行从数据库迁移async function runSecondaryMigrations() { await secondaryDataSource.runMigrations();}// 运行所有数据库迁移async function runAllMigrations() { await Promise.all([ primaryDataSource.runMigrations(), secondaryDataSource.runMigrations(), cacheDataSource.runMigrations(), ]);}数据库切换策略环境变量配置// config/database.tsimport { DataSource } from 'typeorm';export function createDataSource(): DataSource { const dbType = process.env.DB_TYPE || 'mysql'; const commonConfig = { entities: [User, Post], synchronize: false, logging: true, }; switch (dbType) { case 'mysql': return new DataSource({ type: 'mysql', host: process.env.MYSQL_HOST || 'localhost', port: parseInt(process.env.MYSQL_PORT || '3306'), username: process.env.MYSQL_USER || 'root', password: process.env.MYSQL_PASSWORD || 'password', database: process.env.MYSQL_DATABASE || 'myapp', ...commonConfig, }); case 'postgres': return new DataSource({ type: 'postgres', host: process.env.POSTGRES_HOST || 'localhost', port: parseInt(process.env.POSTGRES_PORT || '5432'), username: process.env.POSTGRES_USER || 'postgres', password: process.env.POSTGRES_PASSWORD || 'password', database: process.env.POSTGRES_DATABASE || 'myapp', ...commonConfig, }); case 'sqlite': return new DataSource({ type: 'sqlite', database: process.env.SQLITE_DATABASE || './myapp.db', ...commonConfig, }); default: throw new Error(`Unsupported database type: ${dbType}`); }}// 使用const dataSource = createDataSource();await dataSource.initialize();读写分离配置// 主数据库(写操作)const writeDataSource = new DataSource({ type: 'mysql', name: 'write', host: 'write-db.example.com', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});// 从数据库(读操作)const readDataSource = new DataSource({ type: 'mysql', name: 'read', host: 'read-db.example.com', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User, Post], synchronize: false, logging: true,});// 使用读写分离class UserRepository { async findById(id: number) { // 从从数据库读取 const repository = readDataSource.getRepository(User); return await repository.findOne({ where: { id } }); } async save(user: User) { // 写入主数据库 const repository = writeDataSource.getRepository(User); return await repository.save(user); } async findAll() { // 从从数据库读取 const repository = readDataSource.getRepository(User); return await repository.find(); }}性能优化连接池配置// MySQL 连接池优化const mysqlDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User], synchronize: false, logging: true, extra: { connectionLimit: 20, // 根据应用负载调整 acquireTimeout: 60000, timeout: 60000, waitForConnections: true, queueLimit: 100, },});// PostgreSQL 连接池优化const postgresDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'password', database: 'myapp', entities: [User], synchronize: false, logging: true, extra: { max: 20, // 最大连接数 min: 2, // 最小连接数 idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, },});查询优化// 使用合适的数据库类型async function getBestDatabaseForOperation(operation: string) { switch (operation) { case 'read-heavy': // 使用 PostgreSQL 适合读密集型操作 return postgresDataSource; case 'write-heavy': // 使用 MySQL 适合写密集型操作 return mysqlDataSource; case 'cache': // 使用 MongoDB 适合缓存 return mongoDataSource; case 'analytics': // 使用 PostgreSQL 的窗口函数适合分析 return postgresDataSource; default: return mysqlDataSource; }}// 使用示例const dataSource = await getBestDatabaseForOperation('read-heavy');const repository = dataSource.getRepository(User);const users = await repository.find();TypeORM 的多数据库支持提供了强大的灵活性,让开发者可以根据不同的需求选择最合适的数据库类型,并在同一个应用中同时使用多个数据库。
阅读 0·2月18日 21:08

TypeORM 如何使用验证器?包括 class-validator 的集成和自定义验证器的实现

数据验证是应用程序开发中的重要环节,TypeORM 可以与各种验证器库集成,确保数据的完整性和一致性。本文将详细介绍 TypeORM 中如何使用验证器进行数据验证。验证器基础概念什么是验证器验证器是用于验证数据是否符合特定规则的机制,包括:字段类型验证字段格式验证字段长度验证自定义业务规则验证跨字段验证常用验证器库class-validator: 最流行的 TypeScript 验证器库class-transformer: 用于对象转换和验证joi: 强大的对象模式验证库zod: TypeScript 优先的模式验证库使用 class-validator安装依赖npm install class-validator class-transformernpm install --save-dev @types/class-transformer基本验证示例import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate} from 'typeorm';import { IsEmail, IsNotEmpty, IsString, MinLength, MaxLength, IsInt, Min, Max, IsOptional, IsDateString, IsEnum, ValidateIf, ValidateNested} from 'class-validator';import { Type } from 'class-transformer';@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() @IsNotEmpty({ message: 'Name cannot be empty' }) @IsString({ message: 'Name must be a string' }) @MinLength(2, { message: 'Name must be at least 2 characters' }) @MaxLength(100, { message: 'Name must not exceed 100 characters' }) name: string; @Column({ unique: true }) @IsEmail({}, { message: 'Invalid email format' }) @IsNotEmpty({ message: 'Email cannot be empty' }) email: string; @Column({ nullable: true }) @IsOptional() @MinLength(8, { message: 'Password must be at least 8 characters' }) @MaxLength(100, { message: 'Password must not exceed 100 characters' }) password?: string; @Column({ type: 'int', nullable: true }) @IsOptional() @IsInt({ message: 'Age must be an integer' }) @Min(18, { message: 'Age must be at least 18' }) @Max(120, { message: 'Age must not exceed 120' }) age?: number; @Column({ type: 'enum', enum: ['active', 'inactive', 'suspended'], default: 'active' }) @IsEnum(['active', 'inactive', 'suspended'], { message: 'Invalid status' }) status: string; @Column({ type: 'date', nullable: true }) @IsOptional() @IsDateString({}, { message: 'Invalid date format' }) birthDate?: Date; @BeforeInsert() @BeforeUpdate() async validate() { const errors = await validate(this); if (errors.length > 0) { throw new Error(`Validation failed: ${JSON.stringify(errors)}`); } }}高级验证自定义验证器import { ValidatorConstraint, ValidatorConstraintInterface, registerDecorator, ValidationOptions } from 'class-validator';// 自定义验证器:检查用户名是否唯一@ValidatorConstraint({ name: 'isUsernameUnique', async: true })export class IsUsernameUniqueConstraint implements ValidatorConstraintInterface { async validate(username: string) { // 这里应该查询数据库检查用户名是否唯一 // 示例代码 const userExists = await checkUsernameExists(username); return !userExists; } defaultMessage(args: ValidationArguments) { return 'Username already exists'; }}// 自定义装饰器export function IsUsernameUnique(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsUsernameUniqueConstraint, }); };}// 使用自定义验证器@Entity()export class User { @Column({ unique: true }) @IsUsernameUnique({ message: 'Username already exists' }) username: string;}条件验证import { ValidateIf } from 'class-validator';@Entity()export class User { @Column() @IsNotEmpty() accountType: 'personal' | 'business'; @Column({ nullable: true }) @ValidateIf(o => o.accountType === 'business') @IsNotEmpty({ message: 'Company name is required for business accounts' }) companyName?: string; @Column({ nullable: true }) @ValidateIf(o => o.accountType === 'business') @IsNotEmpty({ message: 'Tax ID is required for business accounts' }) taxId?: string; @Column({ nullable: true }) @ValidateIf(o => o.accountType === 'personal') @IsNotEmpty({ message: 'Personal ID is required for personal accounts' }) personalId?: string;}嵌套对象验证import { ValidateNested, Type } from 'class-transformer';import { IsNotEmpty, IsString, ValidateIf } from 'class-validator';class Address { @IsNotEmpty() @IsString() street: string; @IsNotEmpty() @IsString() city: string; @IsNotEmpty() @IsString() zipCode: string;}@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() @IsNotEmpty() name: string; @Column({ type: 'json', nullable: true }) @ValidateIf(o => o.hasAddress) @ValidateNested() @Type(() => Address) address?: Address; @Column({ default: false }) hasAddress: boolean;}跨字段验证import { ValidatorConstraint, ValidatorConstraintInterface, registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';// 自定义验证器:确认密码匹配@ValidatorConstraint({ name: 'isPasswordMatching', async: false })export class IsPasswordMatchingConstraint implements ValidatorConstraintInterface { validate(password: string, args: ValidationArguments) { const object = args.object as any; return password === object.password; } defaultMessage(args: ValidationArguments) { return 'Passwords do not match'; }}export function IsPasswordMatching(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsPasswordMatchingConstraint, }); };}@Entity()export class User { @Column() @IsNotEmpty() @MinLength(8) password: string; @Column({ nullable: true }) @IsPasswordMatching({ message: 'Passwords do not match' }) confirmPassword?: string;}验证错误处理验证并获取错误import { validate, ValidationError } from 'class-validator';async function createUser(userData: Partial<User>) { const user = new User(); Object.assign(user, userData); const errors = await validate(user); if (errors.length > 0) { // 格式化错误信息 const formattedErrors = this.formatValidationErrors(errors); throw new Error(`Validation failed: ${JSON.stringify(formattedErrors)}`); } // 保存用户 return await userRepository.save(user);}function formatValidationErrors(errors: ValidationError[]): any { const result: any = {}; errors.forEach(error => { const constraints = error.constraints || {}; result[error.property] = Object.values(constraints).join(', '); if (error.children && error.children.length > 0) { result[error.property] = { ...result[error.property], ...this.formatValidationErrors(error.children) }; } }); return result;}// 使用示例try { const user = await createUser({ name: '', email: 'invalid-email', age: 15 });} catch (error) { console.error(error.message); // 输出: Validation failed: {"name":"Name cannot be empty","email":"Invalid email format","age":"Age must be at least 18"}}自定义验证中间件import { validate } from 'class-validator';import { plainToClass } from 'class-transformer';export function validationMiddleware<T extends object>( type: new () => T) { return async (req: any, res: any, next: any) => { const dto = plainToClass(type, req.body); const errors = await validate(dto); if (errors.length > 0) { const formattedErrors = formatValidationErrors(errors); return res.status(400).json({ error: 'Validation failed', details: formattedErrors }); } req.body = dto; next(); };}// 在 Express 中使用import express from 'express';const app = express();app.post('/users', validationMiddleware(User), async (req, res) => { const user = await userRepository.save(req.body); res.json(user); });验证器装饰器详解字符串验证@Entity()export class User { @Column() @IsString() @IsNotEmpty() @MinLength(2) @MaxLength(100) @IsAlphanumeric() name: string; @Column() @IsEmail() @IsLowercase() email: string; @Column() @IsUrl() website?: string; @Column() @IsPhoneNumber(null) // 需要安装 class-validator-phone-number phone?: string;}数字验证@Entity()export class Product { @Column({ type: 'decimal', precision: 10, scale: 2 }) @IsNumber() @Min(0) @Max(999999.99) price: number; @Column({ type: 'int' }) @IsInt() @IsPositive() stock: number; @Column({ type: 'int' }) @IsInt() @IsDivisibleBy(10) quantity: number;}日期验证@Entity()export class Event { @Column({ type: 'date' }) @IsDateString() @IsBefore('endDate') startDate: Date; @Column({ type: 'date' }) @IsDateString() @IsAfter('startDate') endDate: Date; @Column({ type: 'date' }) @IsDateString() @IsFuture() registrationDeadline?: Date;}数组和对象验证@Entity()export class User { @Column({ type: 'simple-array' }) @IsArray() @ArrayNotEmpty() @ArrayMinSize(1) @ArrayMaxSize(10) @IsString({ each: true }) tags: string[]; @Column({ type: 'json', nullable: true }) @IsObject() @IsNotEmptyObject() metadata?: Record<string, any>; @Column({ type: 'simple-array', nullable: true }) @IsArray() @ArrayUnique() @IsEmail({ each: true }) additionalEmails?: string[];}验证最佳实践1. 分层验证// 实体层验证:数据库级别的验证@Entity()export class User { @Column() @IsNotEmpty() @IsString() name: string; @BeforeInsert() @BeforeUpdate() async validateEntity() { const errors = await validate(this); if (errors.length > 0) { throw new Error(`Entity validation failed: ${JSON.stringify(errors)}`); } }}// DTO 层验证:API 请求级别的验证class CreateUserDto { @IsNotEmpty() @IsString() @MinLength(2) @MaxLength(100) name: string; @IsNotEmpty() @IsEmail() email: string; @IsNotEmpty() @MinLength(8) password: string;}// 在服务层使用 DTO 验证async function createUser(dto: CreateUserDto) { const errors = await validate(dto); if (errors.length > 0) { throw new ValidationException(errors); } const user = new User(); Object.assign(user, dto); return await userRepository.save(user);}2. 异步验证@ValidatorConstraint({ name: 'isEmailUnique', async: true })export class IsEmailUniqueConstraint implements ValidatorConstraintInterface { async validate(email: string) { const user = await userRepository.findOne({ where: { email } }); return !user; } defaultMessage() { return 'Email already exists'; }}@Entity()export class User { @Column({ unique: true }) @IsEmailUnique() email: string;}3. 国际化错误消息import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';@ValidatorConstraint({ name: 'customValidator', async: false })export class CustomValidatorConstraint implements ValidatorConstraintInterface { validate(value: any, args: ValidationArguments) { return true; } defaultMessage(args: ValidationArguments) { // 根据语言环境返回不同的错误消息 const locale = args.object['locale'] || 'en'; const messages = { en: 'Custom validation failed', zh: '自定义验证失败', ja: 'カスタム検証に失敗しました' }; return messages[locale] || messages.en; }}4. 性能优化// 避免在验证器中执行耗时操作@ValidatorConstraint({ name: 'isUnique', async: true })export class IsUniqueConstraint implements ValidatorConstraintInterface { private cache = new Map<string, boolean>(); async validate(value: any, args: ValidationArguments) { const cacheKey = `${args.targetName}.${args.property}.${value}`; // 检查缓存 if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } // 执行验证 const result = await this.checkUniqueness(value, args); // 缓存结果 this.cache.set(cacheKey, result); return result; } private async checkUniqueness(value: any, args: ValidationArguments): Promise<boolean> { // 实际的唯一性检查逻辑 return true; }}5. 测试验证器import { validate } from 'class-validator';describe('User Validation', () => { it('should validate valid user', async () => { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 25; const errors = await validate(user); expect(errors.length).toBe(0); }); it('should fail validation for invalid email', async () => { const user = new User(); user.name = 'John Doe'; user.email = 'invalid-email'; user.age = 25; const errors = await validate(user); expect(errors.length).toBeGreaterThan(0); expect(errors[0].constraints).toHaveProperty('isEmail'); }); it('should fail validation for underage user', async () => { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 15; const errors = await validate(user); expect(errors.length).toBeGreaterThan(0); expect(errors[0].constraints).toHaveProperty('min'); });});验证器与其他库集成与 Joi 集成import * as Joi from 'joi';const userSchema = Joi.object({ name: Joi.string().min(2).max(100).required(), email: Joi.string().email().required(), age: Joi.number().integer().min(18).max(120).optional(), password: Joi.string().min(8).required()});@Entity()export class User { @BeforeInsert() @BeforeUpdate() async validateWithJoi() { const { error } = userSchema.validate(this); if (error) { throw new Error(`Validation failed: ${error.details[0].message}`); } }}与 Zod 集成import { z } from 'zod';const userSchema = z.object({ name: z.string().min(2).max(100), email: z.string().email(), age: z.number().int().min(18).max(120).optional(), password: z.string().min(8)});@Entity()export class User { @BeforeInsert() @BeforeUpdate() async validateWithZod() { const result = userSchema.safeParse(this); if (!result.success) { throw new Error(`Validation failed: ${JSON.stringify(result.error.errors)}`); } }}TypeORM 的验证器功能提供了强大的数据验证能力,合理使用验证器可以确保数据的完整性和一致性,提高应用程序的健壮性。
阅读 0·2月18日 19:12

TypeORM 的高级查询技巧有哪些?包括复杂条件、子查询、窗口函数等

TypeORM 提供了强大的查询功能,掌握高级查询技巧可以让你构建出高效、灵活的数据库查询。本文将详细介绍 TypeORM 的各种高级查询技巧。复杂条件查询使用 Brackets 构建复杂条件import { Brackets } from 'typeorm';// 构建复杂的 OR 条件const users = await userRepository .createQueryBuilder('user') .where('user.status = :status', { status: 'active' }) .andWhere( new Brackets(qb => { qb.where('user.name LIKE :name', { name: '%John%' }) .orWhere('user.email LIKE :email', { email: '%john%' }); }) ) .getMany();// 等价 SQL: WHERE user.status = 'active' AND (user.name LIKE '%John%' OR user.email LIKE '%john%')// 多层嵌套条件const users = await userRepository .createQueryBuilder('user') .where( new Brackets(qb => { qb.where('user.age > :minAge', { minAge: 18 }) .andWhere( new Brackets(qb2 => { qb2.where('user.status = :status1', { status1: 'active' }) .orWhere('user.status = :status2', { status2: 'pending' }); }) ); }) ) .getMany();使用 Raw SQL 条件// 使用原始 SQL 表达式const users = await userRepository .createQueryBuilder('user') .where('user.age > :age', { age: 18 }) .andWhere('DATE(user.createdAt) > :date', { date: '2024-01-01' }) .getMany();// 使用函数const users = await userRepository .createQueryBuilder('user') .where('LOWER(user.name) = LOWER(:name)', { name: 'JOHN' }) .getMany();// 使用 CASE WHENconst users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'CASE WHEN user.age >= 18 THEN :adult ELSE :minor END as ageGroup', ]) .setParameter('adult', 'Adult') .setParameter('minor', 'Minor') .getRawMany();子查询使用 EXISTS 子查询// 查询有文章的用户const users = await userRepository .createQueryBuilder('user') .where(qb => { const subQuery = qb .subQuery() .select('1') .from(Post, 'post') .where('post.authorId = user.id') .getQuery(); return `EXISTS ${subQuery}`; }) .getMany();// 等价 SQL: SELECT * FROM user WHERE EXISTS (SELECT 1 FROM post WHERE post.authorId = user.id)// 查询没有文章的用户const users = await userRepository .createQueryBuilder('user') .where(qb => { const subQuery = qb .subQuery() .select('1') .from(Post, 'post') .where('post.authorId = user.id') .getQuery(); return `NOT EXISTS ${subQuery}`; }) .getMany();使用 IN 子查询// 查询文章数量大于 5 的用户const users = await userRepository .createQueryBuilder('user') .where('user.id IN ' + userRepository .createQueryBuilder('user2') .select('user2.id') .from(Post, 'post') .where('post.authorId = user2.id') .groupBy('user2.id') .having('COUNT(post.id) > :count', { count: 5 }) .getQuery() ) .getMany();// 等价 SQL: SELECT * FROM user WHERE user.id IN (SELECT user2.id FROM post WHERE post.authorId = user2.id GROUP BY user2.id HAVING COUNT(post.id) > 5)使用子查询进行计算// 查询每个用户的文章数量const users = await userRepository .createQueryBuilder('user') .leftJoin( (subQuery) => { return subQuery .select('post.authorId', 'authorId') .addSelect('COUNT(*)', 'postCount') .from(Post, 'post') .groupBy('post.authorId'); }, 'postStats', 'postStats.authorId = user.id' ) .addSelect('postStats.postCount', 'postCount') .getMany();// 或者使用子查询表达式const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', `( SELECT COUNT(*) FROM post WHERE post.authorId = user.id ) as postCount`, ]) .getRawMany();聚合和分组基本聚合函数// 统计用户数量const count = await userRepository.count();// 统计活跃用户数量const activeCount = await userRepository.count({ where: { status: 'active' }});// 计算平均年龄const result = await userRepository .createQueryBuilder('user') .select('AVG(user.age)', 'avgAge') .getRawOne();// 计算总和const result = await userRepository .createQueryBuilder('user') .select('SUM(user.score)', 'totalScore') .getRawOne();// 最大值和最小值const result = await userRepository .createQueryBuilder('user') .select('MAX(user.age)', 'maxAge') .addSelect('MIN(user.age)', 'minAge') .getRawOne();GROUP BY 和 HAVING// 按状态分组统计用户数量const result = await userRepository .createQueryBuilder('user') .select('user.status', 'status') .addSelect('COUNT(*)', 'count') .groupBy('user.status') .getRawMany();// 按年龄分组统计const result = await userRepository .createQueryBuilder('user') .select([ 'CASE ' + ' WHEN user.age < 18 THEN :minor ' + ' WHEN user.age < 30 THEN :young ' + ' WHEN user.age < 50 THEN :middle ' + ' ELSE :senior ' + 'END as ageGroup', 'COUNT(*) as count', ]) .setParameter('minor', 'Minor') .setParameter('young', 'Young') .setParameter('middle', 'Middle') .setParameter('senior', 'Senior') .groupBy('ageGroup') .getRawMany();// 使用 HAVING 过滤分组const result = await userRepository .createQueryBuilder('user') .select('user.status', 'status') .addSelect('COUNT(*)', 'count') .groupBy('user.status') .having('COUNT(*) > :minCount', { minCount: 10 }) .getRawMany();// 多列分组const result = await userRepository .createQueryBuilder('user') .select(['user.status', 'user.city']) .addSelect('COUNT(*)', 'count') .groupBy('user.status') .addGroupBy('user.city') .getRawMany();窗口函数使用窗口函数// ROW_NUMBER() - 为每行分配一个唯一的序号const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'ROW_NUMBER() OVER (ORDER BY user.score DESC) as rank', ]) .getRawMany();// RANK() - 相同分数的用户获得相同的排名const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'RANK() OVER (ORDER BY user.score DESC) as rank', ]) .getRawMany();// DENSE_RANK() - 紧密排名,不跳过排名const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'DENSE_RANK() OVER (ORDER BY user.score DESC) as rank', ]) .getRawMany();// LAG() 和 LEAD() - 访问前一行和后一行的数据const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'LAG(user.score) OVER (ORDER BY user.id) as prevScore', 'LEAD(user.score) OVER (ORDER BY user.id) as nextScore', ]) .getRawMany();// SUM() OVER() - 计算累计和const users = await userRepository .createQueryBuilder('user') .select([ 'user.id', 'user.name', 'user.score', 'SUM(user.score) OVER (ORDER BY user.id) as cumulativeScore', ]) .getRawMany();连接查询多表连接// 左连接多个表const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('post.comments', 'comment') .leftJoinAndSelect('comment.author', 'commentAuthor') .where('user.id = :id', { id: 1 }) .getOne();// 自连接const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.manager', 'manager') .where('user.managerId IS NOT NULL') .getMany();// 条件连接const users = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post', 'post.status = :status', { status: 'published' }) .addSelect(['post.id', 'post.title']) .where('user.id = :id', { id: 1 }) .getOne();复杂连接查询// 查询每个用户的最新文章const users = await userRepository .createQueryBuilder('user') .leftJoin( (subQuery) => { return subQuery .select('p1.authorId') .addSelect('p1.id', 'postId') .addSelect('p1.title', 'postTitle') .addSelect('p1.createdAt', 'postCreatedAt') .from(Post, 'p1') .where( `p1.createdAt = ( SELECT MAX(p2.createdAt) FROM post p2 WHERE p2.authorId = p1.authorId )` ); }, 'latestPost', 'latestPost.authorId = user.id' ) .addSelect('latestPost.postTitle', 'latestPostTitle') .addSelect('latestPost.postCreatedAt', 'latestPostCreatedAt') .getMany();排序技巧自定义排序// 使用表达式排序const users = await userRepository .createQueryBuilder('user') .orderBy('CASE user.status WHEN :active THEN 1 WHEN :pending THEN 2 ELSE 3 END', 'ASC') .setParameter('active', 'active') .setParameter('pending', 'pending') .getMany();// 使用函数排序const users = await userRepository .createQueryBuilder('user') .orderBy('LENGTH(user.name)', 'DESC') .getMany();// 多条件排序const users = await userRepository .createQueryBuilder('user') .orderBy('user.status', 'ASC') .addOrderBy('user.createdAt', 'DESC') .addOrderBy('user.name', 'ASC') .getMany();// 随机排序(MySQL)const users = await userRepository .createQueryBuilder('user') .orderBy('RAND()') .getMany();// 随机排序(PostgreSQL)const users = await userRepository .createQueryBuilder('user') .orderBy('RANDOM()') .getMany();NULL 值排序// NULL 值排在最后(MySQL)const users = await userRepository .createQueryBuilder('user') .orderBy('user.age IS NULL', 'ASC') .addOrderBy('user.age', 'ASC') .getMany();// NULL 值排在最前(MySQL)const users = await userRepository .createQueryBuilder('user') .orderBy('user.age IS NULL', 'DESC') .addOrderBy('user.age', 'ASC') .getMany();// 使用 COALESCE 处理 NULLconst users = await userRepository .createQueryBuilder('user') .orderBy('COALESCE(user.age, 0)', 'ASC') .getMany();分页优化基于游标的分页// 使用游标分页提高性能async function getUsersWithCursor(cursor: number, limit: number = 10) { const query = userRepository .createQueryBuilder('user') .orderBy('user.id', 'ASC') .limit(limit + 1); // 多取一条判断是否有下一页 if (cursor) { query.where('user.id > :cursor', { cursor }); } const users = await query.getMany(); const hasNextPage = users.length > limit; if (hasNextPage) { users.pop(); // 移除多取的一条 } return { data: users, nextCursor: hasNextPage ? users[users.length - 1].id : null, hasNextPage };}// 使用示例const page1 = await getUsersWithCursor(null, 10);const page2 = await getUsersWithCursor(page1.nextCursor, 10);基于时间戳的分页// 使用时间戳分页async function getUsersByTimestamp( lastTimestamp: Date | null, limit: number = 10) { const query = userRepository .createQueryBuilder('user') .orderBy('user.createdAt', 'DESC') .limit(limit + 1); if (lastTimestamp) { query.where('user.createdAt < :timestamp', { timestamp: lastTimestamp }); } const users = await query.getMany(); const hasMore = users.length > limit; if (hasMore) { users.pop(); } return { data: users, nextTimestamp: hasMore ? users[users.length - 1].createdAt : null, hasMore };}性能优化技巧使用索引提示// 强制使用特定索引(MySQL)const users = await userRepository .createQueryBuilder('user') .where('user.email = :email', { email: 'john@example.com' }) .setHint('FORCE INDEX (idx_user_email)') .getOne();// 使用索引提示(PostgreSQL)const users = await userRepository .createQueryBuilder('user') .where('user.email = :email', { email: 'john@example.com' }) .setHint('SET enable_seqscan = off') .getOne();避免全表扫描// ❌ 不好的做法:可能导致全表扫描const users = await userRepository .createQueryBuilder('user') .where('LOWER(user.name) = :name', { name: 'john' }) .getMany();// ✅ 好的做法:使用索引友好的查询const users = await userRepository .createQueryBuilder('user') .where('user.name = :name', { name: 'John' }) .getMany();使用 EXISTS 代替 IN// ❌ 不好的做法:使用 IN 可能导致性能问题const users = await userRepository .createQueryBuilder('user') .where('user.id IN :ids', { ids: [1, 2, 3, 4, 5] }) .getMany();// ✅ 好的做法:使用 EXISTSconst users = await userRepository .createQueryBuilder('user') .where(qb => { const subQuery = qb .subQuery() .select('1') .from(Post, 'post') .where('post.authorId = user.id') .getQuery(); return `EXISTS ${subQuery}`; }) .getMany();查询调试查看生成的 SQL// 获取生成的 SQLconst query = userRepository .createQueryBuilder('user') .where('user.id = :id', { id: 1 });const sql = query.getSql();console.log('Generated SQL:', sql);// 执行查询const user = await query.getOne();// 使用日志记录所有查询const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User], logging: true, // 启用日志 maxQueryExecutionTime: 1000, // 记录超过 1 秒的查询});分析查询性能// 使用 EXPLAIN 分析查询async function analyzeQuery(query: string) { const result = await dataSource.query(`EXPLAIN ${query}`); console.log('Query Analysis:', result); // 检查是否使用了索引 const usingIndex = result.some(row => row.type === 'ref' || row.type === 'eq_ref' ); if (!usingIndex) { console.warn('Query not using index'); }}// 使用示例const query = userRepository .createQueryBuilder('user') .where('user.email = :email', { email: 'john@example.com' });await analyzeQuery(query.getSql());TypeORM 的高级查询技巧提供了强大而灵活的数据库操作能力,掌握这些技巧可以让你构建出高效、复杂的数据库查询。
阅读 0·2月18日 19:09