Skip to content

Commit

Permalink
feat: Foreign & Index
Browse files Browse the repository at this point in the history
  • Loading branch information
SALTWOOD committed Dec 14, 2024
1 parent c3a95e4 commit 9d5ee80
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 23 deletions.
6 changes: 3 additions & 3 deletions src/database/Article.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AutoIncrement, PrimaryKey, Table } from "./IDatabase.js";
import { AutoIncrement, Index, PrimaryKey, Table } from "./IDatabase.js";

@PrimaryKey("id")
@Table("articles", `
Expand All @@ -12,9 +12,9 @@ import { AutoIncrement, PrimaryKey, Table } from "./IDatabase.js";
title VARCHAR(24),
background TEXT,
hash CHAR(64),
PRIMARY KEY (id),
INDEX author (author)
PRIMARY KEY (id)
`)
@Index("author", "author")
@AutoIncrement("id")
export class Article {
public author: number = 0;
Expand Down
10 changes: 5 additions & 5 deletions src/database/Comment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Article } from "./Article.js";
import { AutoIncrement, Ignore, PrimaryKey, Table } from "./IDatabase.js";
import { AutoIncrement, Foreign, Ignore, Index, PrimaryKey, Table } from "./IDatabase.js";
import { UserEntity } from "./UserEntity.js";
import { createHash } from "crypto";

Expand All @@ -11,12 +11,12 @@ import { createHash } from "crypto";
article INT,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
hash CHAR(40),
FOREIGN KEY (parent) REFERENCES comments(id),
PRIMARY KEY (id),
INDEX user (user),
INDEX article (article),
INDEX parent (parent)
`)
@Index("user", "user")
@Index("article", "article")
@Index("parent", "parent")
@Foreign("parent", { table: "comments", column: "id" }, "parent_children_constraint", "CASCADE", undefined)
@AutoIncrement("id")
@PrimaryKey("id")
export class Comment {
Expand Down
51 changes: 41 additions & 10 deletions src/database/IDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export const mysqlTableSchemaMap = new Map<Function, string>();
export const mysqlTableNameMap = new Map<Function, string>();
export const mysqlPrimaryKeyMap = new Map<Function, string>();
export const mysqlAutoIncrementMap = new Map<Function, string>();
export const mysqlIndexMap = new Map<Function, { name: string, index: string }[]>();
export const mysqlForeignMap = new Map<Function, { name: string, key: string, target: { table: string, column: string }, on: { delete: string, update: string } }[]>();

// 装饰器用于指定表名和表结构
function Table(tableName: string, mysqlSchema: string) {
Expand Down Expand Up @@ -36,18 +38,47 @@ function Ignore() {
};
}

export { Table, Ignore, AutoIncrement, PrimaryKey };
function Index(name: string, index: string) {
return function (constructor: Function) {
if (!mysqlIndexMap.has(constructor)) {
mysqlIndexMap.set(constructor, []);
}
mysqlIndexMap.get(constructor)?.push({
name,
index
});
};
}

function Foreign(foreignKey: string, foreignTarget: { table: string, column: string }, name: string, onDelete: string = 'CASCADE', onUpdate: string = 'RESTRICT') {
return function (constructor: Function) {
if (!mysqlForeignMap.has(constructor)) {
mysqlForeignMap.set(constructor, []);
}
mysqlForeignMap.get(constructor)?.push({
name,
key: foreignKey,
target: foreignTarget,
on: {
delete: onDelete,
update: onUpdate
}
});
};
}

export { Table, Ignore, AutoIncrement, PrimaryKey, Index, Foreign };

export interface IDatabase {
createTable<T extends object>(type: { new (): T }): Promise<void>;
insert<T extends object>(type: { new (): T }, obj: T): Promise<number>;
select<T extends object>(type: { new (): T }, columns: string[], whereClause?: string, params?: any[], variable?: string): Promise<T[]>;
getEntity<T extends object>(type: { new (): T }, primaryKey: number | string): Promise<T | null>;
getEntities<T extends object>(type: { new (): T }): Promise<T[]>;
update<T extends object>(type: { new (): T }, obj: T): Promise<void>;
remove<T extends object>(type: { new (): T }, obj: T): Promise<void>;
createTable<T extends object>(type: { new(): T }): Promise<void>;
insert<T extends object>(type: { new(): T }, obj: T): Promise<number>;
select<T extends object>(type: { new(): T }, columns: string[], whereClause?: string, params?: any[], variable?: string): Promise<T[]>;
getEntity<T extends object>(type: { new(): T }, primaryKey: number | string): Promise<T | null>;
getEntities<T extends object>(type: { new(): T }): Promise<T[]>;
update<T extends object>(type: { new(): T }, obj: T): Promise<void>;
remove<T extends object>(type: { new(): T }, obj: T): Promise<void>;
close(): Promise<void>;
count<T extends object>(type: { new (): T }, whereClause?: string, params?: any[], variable?: string): Promise<number>;
query<T extends object>(type: { new (): T }, sql: string, params?: any[]): Promise<T[]>;
count<T extends object>(type: { new(): T }, whereClause?: string, params?: any[], variable?: string): Promise<number>;
query<T extends object>(type: { new(): T }, sql: string, params?: any[]): Promise<T[]>;
run(sql: string, params?: any[]): Promise<any>;
}
74 changes: 69 additions & 5 deletions src/database/MySqlHelper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as mysql from 'mysql2/promise';
import { mysqlPrimaryKeyMap, mysqlTableNameMap, mysqlTableSchemaMap, IDatabase, mysqlAutoIncrementMap } from './IDatabase.js';
import { mysqlPrimaryKeyMap, mysqlTableNameMap, mysqlTableSchemaMap, mysqlIndexMap, IDatabase, mysqlAutoIncrementMap, mysqlForeignMap } from './IDatabase.js';

export class MySqlHelper implements IDatabase {
// @ts-ignore
Expand Down Expand Up @@ -101,7 +101,7 @@ export class MySqlHelper implements IDatabase {
const [rows] = await this.mysqlConnection.query(`SHOW TABLES LIKE ?`, [tableName]);
if ((rows as any[]).length > 0) {
// 表存在,检查并更新列类型和缺少的列
await this.updateTableStructure(tableName, schema);
await this.updateTableStructure(new type().constructor, tableName, schema);
} else {
// 表不存在,直接创建
const createTableSQL = `CREATE TABLE IF NOT EXISTS ${tableName} (${schema})`;
Expand All @@ -110,7 +110,7 @@ export class MySqlHelper implements IDatabase {
}

// 检查并更新表结构
private async updateTableStructure(tableName: string, schema: string): Promise<void> {
private async updateTableStructure(constructor: Function, tableName: string, schema: string): Promise<void> {
// 获取现有表的列信息
const [existingColumns] = await this.mysqlConnection.query(`DESCRIBE ${tableName}`) as any[];

Expand Down Expand Up @@ -158,6 +158,70 @@ export class MySqlHelper implements IDatabase {
if (columnsToAdd.length === 0 && columnsToModify.length === 0) {
console.log(`No changes needed for table ${tableName}`);
}

// 修改 INDEX 定义(如果有变化)
const indexMap = mysqlIndexMap.get(constructor) || [];
const existingIndexes = (await this.mysqlConnection.query(`SHOW INDEX FROM ${tableName}`))[0] as { Key_name: string, Column_name: string }[];
const newIndexes = indexMap.filter(i => !existingIndexes.some(e => e.Key_name === i.name && e.Column_name === i.index));
const removingIndexes = existingIndexes.filter(e => !indexMap.some(i => i.name === e.Key_name && i.index === e.Column_name));
if (newIndexes.length > 0) {
for (const index of newIndexes) {
const createIndexSQL = `CREATE INDEX ${index.name} ON ${tableName} (${index.index})`;
await this.mysqlConnection.query(createIndexSQL);
console.log(`Created index ${index.name} on table ${tableName}`);
}
}
if (removingIndexes.length > 0) {
for (const index of removingIndexes) {
const dropIndexSQL = `DROP INDEX ${index.Key_name} ON ${tableName}`;
await this.mysqlConnection.query(dropIndexSQL);
console.log(`Dropped index ${index.Key_name} on table ${tableName}`);
}
}

// 修改 FOREIGN KEY 定义(如果有变化)
const foreignKeys = mysqlForeignMap.get(constructor) || [];
const existingForeignKeys = (await this.mysqlConnection.query(`
SELECT
kcu.CONSTRAINT_NAME,
kcu.TABLE_NAME,
kcu.COLUMN_NAME,
kcu.REFERENCED_TABLE_NAME,
kcu.REFERENCED_COLUMN_NAME,
rc.UPDATE_RULE,
rc.DELETE_RULE
FROM
information_schema.KEY_COLUMN_USAGE kcu
JOIN
information_schema.REFERENTIAL_CONSTRAINTS rc
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
WHERE
kcu.REFERENCED_TABLE_NAME IS NOT NULL AND kcu.TABLE_NAME = ?
` , [tableName]))[0] as {
CONSTRAINT_NAME: string,
TABLE_NAME: string,
COLUMN_NAME: string,
REFERENCED_TABLE_NAME: string,
REFERENCED_COLUMN_NAME: string,
UPDATE_RULE: string,
DELETE_RULE: string
}[];
const newForeignKeys = foreignKeys.filter(fk => !existingForeignKeys.some(e => e.CONSTRAINT_NAME === fk.name && e.COLUMN_NAME === fk.key && e.REFERENCED_TABLE_NAME === fk.target.table && e.REFERENCED_COLUMN_NAME === fk.target.column));
const removingForeignKeys = existingForeignKeys.filter(e => !foreignKeys.some(fk => fk.name === e.CONSTRAINT_NAME && fk.key === e.COLUMN_NAME && fk.target.table === e.REFERENCED_TABLE_NAME && fk.target.column === e.REFERENCED_COLUMN_NAME));
if (newForeignKeys.length > 0) {
for (const fk of newForeignKeys) {
const createForeignKeySQL = `ALTER TABLE ${tableName} ADD CONSTRAINT ${fk.name} FOREIGN KEY (${fk.key}) REFERENCES ${fk.target.table} (${fk.target.column}) ON UPDATE ${fk.on.update} ON DELETE ${fk.on.delete}`;
await this.mysqlConnection.query(createForeignKeySQL);
console.log(`Created foreign key ${fk.name} on table ${tableName}`);
}
}
if (removingForeignKeys.length > 0) {
for (const fk of removingForeignKeys) {
const dropForeignKeySQL = `ALTER TABLE ${tableName} DROP FOREIGN KEY ${fk.CONSTRAINT_NAME}`;
await this.mysqlConnection.query(dropForeignKeySQL);
console.log(`Dropped foreign key ${fk.CONSTRAINT_NAME} on table ${tableName}`);
}
}
}

// 插入数据(MySQL)
Expand Down Expand Up @@ -185,7 +249,7 @@ export class MySqlHelper implements IDatabase {
Object.assign(entity, row);
return entity;
});

return rows as T[];
}

Expand Down Expand Up @@ -280,7 +344,7 @@ export class MySqlHelper implements IDatabase {
return (await this.mysqlConnection.query(sql, params))[0];
}

public async query<T extends object>(type: { new (): T; }, sql: string, params?: any[]): Promise<T[]> {
public async query<T extends object>(type: { new(): T; }, sql: string, params?: any[]): Promise<T[]> {
const tableName = this.getTableNameByConstructor(type);
const [rows] = await this.mysqlConnection.query(sql, params);
return (rows as T[]).map((row: T) => {
Expand Down

0 comments on commit 9d5ee80

Please sign in to comment.