Asentinel-orm הוא כלי ORM קל משקל הנבנה על גבי Spring JDBC, בעיקר על JdbcTemplate
. לכן, הוא מכיל את רוב התכונות שמצפים מ ORM בסיסי, כמו יצירת SQL, טעינה עצלה, וכו'
על ידי השימוש ב־JdbcTemplate
, זה אומר שהוא מאפשר השתתפות בעסקאות שניהן נמצאות בניהול Spring, וניתן לשלב אותו בקלות בכל פרויקט שכבר משתמש ב־JdbcTemplate
כדי להתקשר עם בסיס הנתונים
מאז 2015, asentinel-orm נמצא בשימוש מוצלח במספר יישומים ומשתפר באופן קבוע כפי שנדרש על ידי צרכי העסק. בקיץ של 2024, זה הפך רשמית לפרויקט קוד פתוח, מה שאנו סופרים שיאציל את התפתחותו ויעצים את מספר התורמים
במאמר זה, יתוכנן יישום דוגמה כדי לציין כמה מתכונות ORM עיקריות:
- הגדרה פשוטה
- עיצוב יישות דומיין ישיר דרך אנוטציות מותאמות
- כתיבה קלה ובטוחה של הצהרות SQL פשוטות
- יצירת הצהרות SQL אוטומטיות
- סכמה דינמית (היישות מתרוככות עם תכונות ריצה נוספות, מאוחסנות ונקראות בלי שינויים בקוד)
יישום
הגדרה
- Java 21
- Spring Boot 3.4.0
- asentinel-orm 1.70.0
- מסד נתונים H2
הגדרה
כדי להתקשר עם ה־asentinel-orm ולנצל את הפונקציונליות שלו, נדרשת התוכנית הבאה של OrmOperations
.
כפי שצוין ב־JavaDoc, זו הממשק המרכזי לביצוע פעולות ORM, ואין צורך או דרישה ליישומה בקוד הלקוח באופן ספציפי.
היישום הדוגמא כולל את קוד ההגדרה ליצירת Bean מסוג זה.
public OrmOperations orm(SqlBuilderFactory sqlBuilderFactory,
JdbcFlavor jdbcFlavor, SqlQuery sqlQuery) {
return new OrmTemplate(sqlBuilderFactory, new SimpleUpdater(jdbcFlavor, sqlQuery));
}
OrmOperations
כוללת שני ממשקים אב:
- SqlBuilderFactory – יוצר
SqlBuilder
שיכולים להיות משמשים ליצירת שאילתות SQL.SqlBuilder
מסוגל לייצר באופן אוטומטי חלקים של השאילתה, למשל, את זה שבוחר את העמודות. סעיף ה־where, סעיף ה־order by, תנאים נוספים, והעמודות הממשיות ניתן להוסיף באמצעות שיטות ממחלקתSqlBuilder
כמו כן. בחלק הבא של סעיף זה מוצגת דוגמה לשאילתה שנוצרה על ידיSqlBuilder
. - Updater – משמש לשמירת יישויות בטבלאות מסד הנתונים המתאימות שלהן. הוא יכול לבצע הכנסות או עדכונים בהתאם לכך אם הישות נוצרה חדש או כבר קיימת. קיים ממשק אסטרטגיה בשם
NewEntityDetector
שמשמש לקביעה האם יישות היא חדשה. ברירת המחדל היא להשתמש ב־SimpleNewEntityDetector
.
כל השאלות שנוצרות על ידי ה-ORM מבוצעות באמצעות SqlQueryTemplate
אינסטנס, שדורש עוד JdbcOperations
/JdbcTemplate
כדי לפעול. בסופו של דבר, כל השאלות מגיעות אל הJdbcTemplate
הישן והטוב דרכו הן מבוצעות תוך כדי השתתפות בעסקאות של Spring, בדיוק כמו כל JdbcTemplate
ביצוע ישיר.
מבנים ולוגיקה SQL ספציפיים לבסיס הנתונים מסופקים באמצעות מימושים של ממשק JdbcFlavor
, שמוזרקים לרוב הבנים שהוזכרו למעלה. במאמר זה, כיוון שבסיס הנתונים H2 משמש, מוגדר מימוש H2JdbcFlavor
.
ההגדרה המלאה של ה-ORM כחלק מהיישום לדוגמה היא OrmConfig
.
מימוש
מודל הדומיין הניסי שמוצג על ידי היישום לדוגמה הוא פשוט ומורכב משתי ישויות – יצרני מכוניות ודגמי מכוניות. המייצגים בדיוק מה ששמותיהם מציינים, הקשר ביניהן ברור: יצרן מכוניות אחד עשוי להיות בעל מספר דגמי מכוניות.
בנוסף לשמו, יצרן המכוניות מעושר באטריבוטים (עמודות) שמוזנים על ידי משתמש היישום באופן דינמי בזמן ריצה. המקרה בשימוש המוצג הוא פשוט:
- המשתמש מתבקש לספק את השמות והסוגים הרצויים עבור המאפיינים הדינמיים
- זוג יצרני רכב נוצרים עם ערכים מוחלטים עבור המאפיינים הדינמיים שנוספו מראש, ואז
- הישויות נטענות חזרה המתוארות על ידי המאפיינים ההתחלתיים והמוגדרים בזמן ריצה
הישויות ההתחלתיות ממופות באמצעות טבלאות מסד הנתונים למטה:
CREATE TABLE CarManufacturers (
ID INT auto_increment PRIMARY KEY,
NAME VARCHAR(255)
);
CREATE TABLE CarModels(
ID INT auto_increment PRIMARY KEY,
CarManufacturer int,
NAME VARCHAR(255),
TYPE VARCHAR(15),
foreign key (CarManufacturer) references CarManufacturers(id)
);
המחלקות התחום המתאימות מודקרות באונוטציות ספציפיות ל-ORM להגדרת המיפויים לטבלאות מסד הנתונים לעיל.
"CarManufacturers") (
public class CarManufacturer {
"id") (
private int id;
"name") (
private String name;
parentRelationType = RelationType.MANY_TO_ONE, (
fkName = CarModel.COL_CAR_MANUFACTURER,
fetchType = FetchType.LAZY)
private List<CarModel> models = Collections.emptyList();
...
}
"CarModels") (
public class CarModel {
public static final String COL_CAR_MANUFACTURER = "CarManufacturer";
"id") (
private int id;
"name") (
private String name;
"type") (
private CarType type;
fkName = COL_CAR_MANUFACTURER, fetchType = FetchType.LAZY) (
private CarManufacturer carManufacturer;
...
}
public enum CarType {
CAR, SUV, TRUCK
}
מספר שקולות:
@Table
– ממפה (מקשרת) את המחלקה לטבלת מסד הנתונים@PkColumn
– ממפה את ה-id
(זיהוי ייחודי) למפתח ראשי של הטבלה@Column
– ממפה איבר במחלקה לעמודת טבלה@Child
– מגדירה את היחס עם ישות אחרת- איברים שנמרקו באמצעות
@Child
– מוגדרים להיטען באיטיות - עמודת טבלת
type
– ממופה לשדהenum
–CarType
כדי שמחלקת CarManufacturer
תומכת במאפיינים המוגדרים בזמן ריצה (ממופים לעמודות טבלה שהוגדרו בזמן ריצה), מתווספת מחלקת מחלקת אב כזו כמו בדוגמה למטה:
public class CustomFieldsCarManufacturer extends CarManufacturer
implements DynamicColumnsEntity<DynamicColumn> {
private final Map<DynamicColumn, Object> customFields = new HashMap<>();
...
public void setValue(DynamicColumn column, Object value) {
customFields.put(column, value);
}
public Object getValue(DynamicColumn column) {
return customFields.get(column);
}
...
}
מחלקה זו מאחסנת את המאפיינים (שדות) המוגדרים בזמן ריצה בתוך Map
. האינטראקציה בין ערכי השדה בזמן ריצה ל-ORM מתקיימת דרך המימוש של ממשק ה-DynamicColumnEntity
.
public interface DynamicColumnsEntity<T extends DynamicColumn> {
void setValue(T column, Object value);
Object getValue(T column);
}
setValue()
– משמש להגדרת ערך של העמודה המוגדרת בזמן ריצה כאשר זו נקראת מהטבלהgetValue()
– משמש לשימוש לשימוש בערך של עמודה שהוגדרה בזמן ריצה כאשר זו שמורה בטבלה
הDynamicColumn
מפתח תכונות מוגדרות בזמן ריצה לעמודות התואמות בדיוק כמו ההערה @Column
ממפה חברים המוכרים בזמן קידום הזמן.
בעת הפעלת היישום, נפעל CfRunner
. מתבקש המשתמש להזין שמות וסוגים עבור התכונות המותאמות הדינמיות המעשירות את היישות CarManufacturer
(לצורך פשטות, נתמך רק בסוגי int
ו- varchar
).
עבור כל זוג שם-סוג, נפעל פקודת DML כך שניתן להוסיף את העמודות החדשות לטבלת מסד הנתונים של CarManufacturer
. השיטה הבאה (המוגדרת ב- CarService
) בוצעת את הפעולה.
public void addManufacturerField(String name, String type) {
orm.getSqlQuery()
.update("alter table CarManufacturers add column " + name + " " + type);
}
כל תכונת קלט מתוודעת כDefaultDynamicColumn
, מימוש הפניה של DynamicColumn
.
כאשר כל התכונות מוגדרות, שני יצרני רכב מוסיפים למסד הנתונים, כאשר המשתמש מספק ערכים עבור כל תכונה כזו.
Map<DynamicColumn, Object> dynamicColumnsValues = new HashMap<>();
for (DynamicColumn dynamicColumn : dynamicColumns) {
// read values for each dynamic attribute
...
}
CustomFieldsCarManufacturer mazda = new CustomFieldsCarManufacturer("Mazda", dynamicColumnsValues);
carService.createManufacturer(mazda, dynamicColumns);
השיטה הבאה (המוגדרת ב- CarService
) מייצרת באמת את היישות דרך ORM.
public void createManufacturer(CustomFieldsCarManufacturer manufacturer, List<DynamicColumn> attributes) {
orm.update(manufacturer, new UpdateSettings<>(attributes, null));
}
הגרסה עם 2 הפרמטרים של OrmOperations
update()
נקראת, המאפשרת להעביר מופע של UpdateSettings
ולתקשר ל-ORM בעת הביצוע שיש ערכים המוגדרים בזמן ריצה שצריכים להישמר.
לבסוף, נוצרות שתי דגמי מכוניות, המתאימות לאחד מיצרני הרכב שהוספו קודם לכן.
CarModel mx5 = new CarModel("MX5", CarType.CAR, mazda);
CarModel cx60 = new CarModel("CX60", CarType.SUV, mazda);
carService.createModels(mx5, cx60);
המתודולוגיה למטה (המוצהרת בCarService
) למעשה יוצרת את הישויות באמצעות ה-ORM, הפעם תוך שימוש במתודה OrmOperations
update() לשמירת ישויות ללא מאפיינים דינמיים. לשם נוחות, נוצרות ישויות מרובות בשיחה אחת.
public void createModels(CarModel... models) {
orm.update(models);
}
כשלב אחרון, אחד מהיצרנים שנוצרו נטען חזרה לפי שמו באמצעות שאילתה שנוצרה על ידי ה-ORM.
CarManufacturer mazda1 = carService.findManufacturerByName("Mazda", dynamicColumns);
readOnly = true) (
public CarManufacturer findManufacturerByName(String name, List<DynamicColumn> attributes) {
return orm.newSqlBuilder(CustomFieldsCarManufacturer.class)
.select(
AutoEagerLoader.forPath(CarManufacturer.class, CarModel.class),
new DynamicColumnsEntityNodeCallback<>(
new DefaultObjectFactory<>(CustomFieldsCarManufacturer.class),
attributes
)
)
.where().column("name").eq(name)
.execForEntity();
}
כמה הסברים לגבי המתודה שהוגדרה למעלה שווים לעשות.
המתודה OrmOperations
newSqlBuilder()
יוצרת מופע של SqlBuilder
, וכפי ששמו מרמז, ניתן להשתמש בו ליצירת שאילתות SQL. המתודה SqlBuilder
select()
יוצרת את החלק select from table של השאילתה, בעוד שהשאר (where, order by) חייבים להתווסף. חלק השאילתה יכול להיות מותאם אישית על ידי העברת מופעים של EntityDescriptorNodeCallback
(פרטים על EntityDescriptorNodeCallback
עשויים להיות נושא למאמר עתידי).
כדי להודיע ל-ORM שהתוכנית היא לבחור ולמפתח עמודות שהוגדרו בזמן ריצה, יש להעביר DynamicColumnsEntityNodeCallback
עם . יחד איתו, מסופק AutoEagerLoader
עם כדי שה-ORM יבין לטעון באופן מידי את רשימת ה-CarModel
שקשורים ליצרן. בכל זאת, זה אינו קשור למאפיינים שהוגדרו בזמן ריצה, אך זה מדגיש כיצד ניתן לטעון באופן מידי חבר ילד.
מסקנה
בעוד ייתכן כי ישנן דרכים אחרות לעבוד עם עמודות שהוגדרו בזמן ריצה כאשר הנתונים מאוחסנים במסדי נתונים רצופים, הגישה שמוצגת במאמר זה יש לה מערך של עמודות בסיסיות שנקראים/נכתבים באמצעות שאילתות SQL סטנדרטיות שיוצרו ישירות על ידי ה-ORM.
לא הייתה די נדירה כאשר היינו יכולים לדון ב-"הקהילה" ב-asentinel-orm, הסיבות להפיתוח של כלי כזה. כללית, מראש הפתים המפתחים התבררו כקשות ושומרים כאשר נדוש ב-ORM מותאמת אישית, שואלים למה לא להשתמש ב-Hibernate או במימושי JPA אחרים.
במקרה שלנו, הדריבר העיקרי היה הצורך בדרך מהירה, גמישה ופשוטה לעבוד עם מספר גדול לפעמים של מאפיינים (עמודות) שהוגדרו בזמן ריצה עבור ישויות שהן חלק מתחום העסקים. בשבילנו, התברר שזהו הדרך הנכונה. היישומים פועלים באופן חלק בייצור, הלקוחות מרוצים מהמהירות והביצועים שהושגו, והמפתחים מרגישים נוח ויצירתיים עם ה- API האינטואיטיבי.
כאשר הפרויקט כעת זמין כקוד פתוח, זה מאוד קל לכל מי שמתעניין לעיין בו, לייצר דעה אובייקטיבית עליו, ואף, למה לא, לעשות fork לו, לפתוח PR, ולתרום.
משאבים
Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm