Development of the
safeorm
has not been completed yet.Until the completion, only interfaces and test automation programs are being provided. When the development has been completed, its 1st version
0.1.0
would be published. Before the completion, version of thesafeorm
would keep the0.0.x
.
npm install --save safeorm
The ultimate Safe ORM library for the TypeScript.
- Repository: https://github.com/samchon/safeorm
Guide Documents: https://github.com/samchon/safeorm/wikiAPI Documents: https://samchon.github.io/safeorm/api- Template Project: https://github.com/samchon/backend
With the SafeORM, you can define database and programmable type at the same time through the TMP (Type Meta Programming). Also, such type would be utilized in the whole SafeORM components level. Therefore, you can be helped by auto-completion with type hint when writing SQL query or planning App join.
Furthermore, you don't need to worry about any type of mistake when writing SQL query or planning App join. Your mistake would be caught in the compilation level. Therefore, if you type a wrong word on the SQL query, it would be enhanced by IDE through the red underline.
Unlike other ORM libraries who've to define DB and programmable type duplicatedly and cause critical runtime error by not supporting the TMP, SafeORM supports those features through below components and they make the DB development to be much safer. Read below components and feel how safe it is.
- Entity: Define DB and TypeScript type at the time by TMP
- QueryBuilder: Auto completion with type hint
- bindAppJoin: Lazy app join binder, extremely easy but powerful performance
- AppJoinBuilder: Eager app join builder, same grammer with the QueryBuilder
- JsonBuilder: JSON converter without any SQL query
- InsertCollection: Massive insertion without considering any dependency relationship
Template Meta Programming
As you can see from the below example code, SafeORM supports the TMP (Type Meta Programming) ORM who can define database type and programmable type at the same time. When you define a column with database type, SafeORM would convert it to the programmable type automatically.
Such automatic conversion even works for the relationship type. If you define a dependency relationship of the database, SafeORM converts it to the programmable relationship type, too. As you can see from the below, relationship functions like Belongs.ManyToOne
and Has.ManyToMany
define the database relationships and programmable accessors, at the same time.
Also, such Entity
types can be utilized in whole components of the SafeORM. For an example, QueryBuilder realizes the auto completion with type hint by analyzing the target Entity
types.
import safe from "safeorm";
import { BbsArticleTag } from "./BbsArticleTag";
import { BbsComment } from "./BbsComment";
import { BbsGroup } from "./BbsGroup";
export class BbsArticle
{
//----
// COLUMNS
//----
// BE `string`
public readonly id = safe.PrimaryGeneratedColumn("uuid");
// BE `Belongs.ManyToOne<BbsGroup, { nullable: true }>`
//
// `group.id` -> `string|null`
// `group.get()` -> `Promise<BbsGroup|null>`
public readonly group = safe.Belongs.ManyToOne
(
() => BbsGroup,
{ nullable: true, index: true }
);
// BE `string`
public readonly title = safe.Column("varchar");
// BE `string | null`
public readonly sub_title = safe.Column("varchar", { nullable: true });
public readonly title = safe.Column("varchar"); // `string`
public readonly body = safe.Column("text"); // `string`
public readonly created_at = safe.CreateDateColumn("datetime"); // `Date`
public readonly updated_at = safe.UpdateDateColumn("datetime"); // `Date`
public readonly deleted_at = safe.DeleteDateColumn("datetime"); // `Date | null`
// BE `number`
public hit = safe.Column("int");
//----
// HAS
//----
// BE `Has.OneToMany<BbsArticleTag>`
//
// `tags.get() -> Promise<BbsArticleTag>`
public readonly tags = safe.Has.OneToMany
(
() => BbsArticleTag,
tag => tag.article
);
// BE `Has.OneToMany<BbsComment>`
public readonly comments = safe.Has.OneToMany
(
() => BbsComment,
comment => comment.article
);
// BE `Has.ManyToMany`
//
// `files.get() -> Promise<AttachmentFile>`
public readonly files = safe.Has.ManyToMany
(
() => AttachmentFile,
() => BbsArticleFile,
router => router.file,
router => router.article,
(x, y) => x.router.sequence - y.router.sequence // SORT FUNCTION
);
}
safe.Index(BbsArticle, ["created_at", "deleted_at"]); // COMPOSITE INDEX
safe.Index(BbsArticle, "title"); // SINGULAR INDEX
safe.Table(BbsArticle); // BE TABLE
Extremely safe with the TMP.
When you've defined some Entities, you can compose SQL query very easily and safely by this QueryBuilder
. The QueryBuilder
analyzes those Entities and supports the auto completion with type hint.
Also, some mistakes like mis-written column name would be automatically detected in the compilation level. Therefore, you don't need to worry about any type of mistake when wrting the SQL query. All of the mistakes would be enhanced by IDE by the red underline.
Look at the below gif image and feel how strong it is. Other ORM libraries like TypeORM never can provide such beautiful TMP (Type Meta Programming). They may cause the critical runtime error for the mis-writiten SQL query.
export async function test_safe_query_builder(): Promise<void>
{
const group: BbsGroup = await BbsGroup.findOneOrFail();
const category: BbsCategory = await BbsCategory.findOneOrFail();
const stmt: safe.SelectQueryBuilder<BbsQuestionArticle> = safe
.createJoinQueryBuilder(BbsQuestionArticle, question =>
{
question.innerJoin("base", article =>
{
article.innerJoin("group");
article.innerJoin("category");
article.innerJoin("__mv_last").innerJoin("content");
});
question.leftJoin("answer")
.leftJoin("base", "AA")
.leftJoin("__mv_last", "AL")
.leftJoin("content", "AC");
})
.andWhere(...BbsArticle.getWhereArguments("group", group))
.andWhere(...BbsCategory.getWhereArguments("code", "!=", category.code))
.select([
BbsArticle.getColumn("id"),
BbsGroup.getColumn("name", "group"),
BbsCategory.getColumn("name", "category"),
BbsArticle.getColumn("writer"),
BbsArticleContent.getColumn("title"),
BbsArticle.getColumn("created_at"),
BbsArticleContent.getColumn("created_at", "updated_at"),
BbsArticle.getColumn("AA.writer", "answer_writer"),
BbsArticleContent.getColumn("AA.title", "answer_title"),
BbsArticle.getColumn("AA.created_at", "answer_created_at"),
]);
}
Extremely easy but powerful performance.
The bindAppJoin
is a function who binds an ORM object (or objects) to perform the lazy application level join. With the lazy app join binding, relationship accessors would never call the repeated SELECT
query for the same type. The SELECT
query would be occured only when same type of the relationship accessor has been called at fisrt.
If that description is hard to understand, read the below code, then you may understand. As you can see from the below, the example code called relationshiop accessor for only one instance topArticle: BbsArticle
. However, another BbsArticle
instances also performs the application level join at the same time and never calls the SELECT
query again. It's the lazy app join.
Also, as you can see from the below description and benchmark result of the App join, the App join is much fater and consumes much fewer resources than the DB join query. Even implementation code of the App join is much easier than the DB join in this SafeORM library. Therefore, I say with condidence, "There's no reason to denying this bindAppJoin
function".
The application level join means that joining related records are done not by the DB join query, but through the manual application code using the HashMap with their primary and foreign key values mapping.
Comparing those DB join query and application level joining, the application level joining consumes much fewer resources and its elapsed time is also much shorter than the DB join query. Those differences grow up whenever the join relationship be more compliate.
Type DB Join App Join Records 2,258,000 165 Elapsed Time 8.07508 0.00262
export async function test_lazy_app_join(): Promise<void>
{
const group: BbsGroup = await BbsGroup.findOneOrFail();
safe.bindAppJoin(group);
const articleList: BbsArticle[] = await group.articles.get();
const topArticle: BbsArticle = articleList[0];
// APP-JOIN WOULD BE DONE
await topArticle.comments.get();
await topArticle.files.get();
await topArticle.tags.get();
// ANY SELECT QUERY WOULD BE OCCURED
await must_not_query_anything("bindAppJoin", async () =>
{
for (const article of articleList)
{
await article.comments.get();
await article.files.get();
await article.tags.get();
}
});
}
With the AppJoinBuilder
class, you can implement eager application level join.
Also, grammer of the AppJoinBuilder
is exactly same with the QueryBuilder
. Therefore, you can swap QueryBuilder
and AppJoinBuilder
very simply without any cost. Thus, you can just select one of them suitable for your case.
export async function test_app_join_builder(): Promise<void>
{
const builder: safe.AppJoinBuilder<BbsReviewArticle> = safe
.createAppJoinBuilder(BbsReviewArticle, review =>
{
review.join("base", article =>
{
article.join("group");
article.join("category");
article.join("contents", content =>
{
content.join("reviewContent");
content.join("files");
});
article.join("comments").join("files");
});
});
}
Furthermore, you've determined to using only the AppJoinBuilder
, you can configure it much safely. With the AppJoinBuilder.initialize()
method, you've configure all of the relationship accessors, and it prevents any type of ommission by your mistake.
export async function test_app_join_builder_initialize(): Promise<void>
{
const builder = safe.AppJoinBuilder.initialize(BbsGroup, {
articles: safe.AppJoinBuilder.initialize(BbsArticle, {
group: undefined,
review: safe.AppJoinBuilder.initialize(BbsReviewArticle, {
base: undefined,
}),
category: safe.AppJoinBuilder.initialize(BbsCategory, {
articles: undefined,
children: undefined,
parent: "recursive"
}),
contents: safe.AppJoinBuilder.initialize(BbsArticleContent, {
article: undefined,
files: "join"
}),
comments: safe.AppJoinBuilder.initialize(BbsComment, {
article: undefined,
files: "join"
}),
tags: "join",
__mv_last: undefined,
question: undefined,
answer: undefined,
})
});
}
JSON converter without any SQL query
In the SafeORM, when you want to load DB records and convert them to a JSON data, you don't need to write any SELECT
or JOIN
query. You also do not need to consider any performance tuning. Just write down the ORM -> JSON
conversion plan, then SafeORM will do everything.
The JsonSelectBuilder
is the class doing everything. It will analyze your JSON conversion plan, and compose the JSON conversion method automatically with the exact JSON type what you want. Furthermore, the JsonSelectBuilder
finds the best (applicataion level) joining plan by itself.
Below code is an example converting ORM model class instances to JSON data with the JsonSelectBuilder
. As you can see, there's no special script in the below code, but only the conversion plan exists. As I've mentioned, JsonSelectBuilder
will construct the exact JSON type by analyzing your conversion plan. Also, the performance tuning would be done automatically.
Therefore, just enjoy the JsonSelectBuilder
without any worry.
export async function test_json_select_builder(models: BbsGroup[]): Promise<void>
{
const builder = BbsGroup.createJsonSelectBuilder
({
articles: BbsArticle.createJsonSelectBuilder
({
group: safe.DEFAULT, // ID ONLY
category: BbsCategory.createJsonSelectBuilder
({
parent: "recursive" as const, // RECURSIVE JOIN
}),
tags: BbsArticleTag.createJsonSelectBuilder
(
{},
tag => tag.value // OUTPUT CONVERSION BY MAPPING
),
contents: BbsArticleContent.createJsonSelectBuilder
({
files: "join" as const
}),
})
});
// GET JSON DATA FROM THE BUILDER
const raw = await builder.getMany(models);
// THE RETURN TYPE IS ALWAYS EXACT
// THEREFORE, TYPEOF "RAW" AND "I-BBS-GROUP" ARE EXACTLY SAME
const regular: IBbsGroup[] = raw;
const inverse: typeof raw = regular;
}
Massive insertion without considering any dependency relationship
When you want to execute INSERT
query for lots of records of plural tables, you've to consider dependency relationships. Also, you may construct extended SQL query manually by yourself, if you're interested in the performance tuning.
However, with the InsertCollection
class provided by this SafeORM, you don't need to consider any dependcy relationship. You also do not need to consider any performance tuning. The InsertCollection
will analyze the dependency relationships and orders the insertion sequence automatically. Also, the InsertCollection
utilizes the extended insertion query for the optimizing performance.
import safe from "safeorm";
import std from "tstl";
export async function archive
(
comments: BbsComment[],
questions: BbsQuestionArticle[],
reviews: BbsArticleReview[],
groups: BbsGroup[],
files: AttachmentFile[],
answers: BbsAnswerArticle[],
categories: BbsCategory[],
comments: BbsComment[],
articles: BbsArticle[],
contents: BbsArticleContent[],
tags: BbsArticleTag[],
): Promise<void>
{
// PREPARE A NEW COLLECTION
const collection: safe.InsertCollection = new safe.InsertCollection();
// PUSH TABLE RECORDS TO THE COLLECTION WITH RANDOM SHULFFLING
const massive = [
comments,
questions,
reviews,
groups,
files,
answers,
comments,
articles,
contents,
tags
];
std.ranges.shuffle(massive);
for (const records of massive)
collection.push(records);
// PUSH INDIVIDUAL RECORDS
for (const category of categories)
collection.push(category);
// EXECUTE THE INSERT QUERY
await collection.execute();
}