mythix-orm 1.2.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/Associations.md +307 -250
- package/docs/QueryEngine.md +426 -0
- package/lib/connection/query-generator-base.js +4 -0
- package/lib/query-engine/field-scope.js +8 -4
- package/lib/query-engine/model-scope.js +0 -4
- package/lib/query-engine/query-engine.js +2 -11
- package/lib/types/virtual/model-type.js +1 -1
- package/package.json +1 -1
package/docs/Associations.md
CHANGED
|
@@ -2,27 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Mythix ORM makes associations really easy.
|
|
4
4
|
|
|
5
|
+
I many other ORMs you need to define how two models are related... and what fields are related on those models, and if the relation ship is polymorphic, and if it should load related models while accessing the relation, and if there is a default scope that should be applied, and...
|
|
6
|
+
|
|
7
|
+
Lame.
|
|
8
|
+
|
|
9
|
+
Let's not repeat that design pattern.
|
|
10
|
+
|
|
11
|
+
Instead, in Mythix ORM, you define relationships *with* queries. This means all that garbage you need to manually define in other ORMs is consistently and conveniently defined all in one place: the relationship query itself. This can include a polymorphic relation, a projection and related models to load, a "default scope", and everything else that can be done in other ORMs, in a simple and intuitive interface.
|
|
12
|
+
|
|
13
|
+
For this to work, when you define a relationship using the `Types.Model` or `Types.Models` types, you always define two parameters: 1) The target model of the relationship, and 2) The "query provider", which is a simple method that returns the query for the relationship.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
5
17
|
Fields in a Mythix ORM model can be either "virtual" or "concrete".
|
|
6
18
|
|
|
7
19
|
Concrete fields are backed by storage (the database), and will have a direct value they can be associated with.
|
|
8
20
|
|
|
9
21
|
Virtual fields are not backed by storage (at least not directly), and instead will dynamically fetch their value.
|
|
10
22
|
|
|
11
|
-
A field
|
|
23
|
+
A field in Mythix ORM with be "concrete" or "virtual" simply based on its type. For example, the types `Types.Model` and `Types.Models` will by nature define a virtual field. These types define relations to other tables, and so don't have a 1x1 concrete field in the DB themselves, and instead will pull data based on the relationship defined by the type.
|
|
12
24
|
|
|
13
25
|
Before we get started, there are a few things to keep in mind:
|
|
14
26
|
|
|
15
27
|
1. `Types.Model` is used for a 1x1 relationship
|
|
16
28
|
2. `Types.Models` is used for a one-to-many, or a many-to-many relationship
|
|
17
29
|
3. These two types specify field relationships
|
|
18
|
-
4. In field relationships, it is
|
|
19
|
-
5.
|
|
20
|
-
6. In field relationships, if a model isn't defined, then `Self` ("this model") is assumed
|
|
21
|
-
7. In field relationships, if a field isn't defined, then the target model's primary key is assumed
|
|
22
|
-
8. Field relationships work by defining a "target", and a "value provider" (where to get the value to match against the target)
|
|
23
|
-
9. All concrete fields must be manually defined on all models. There is no "automagic" or "hidden" fields that the relationships themselves will create. Said another way, **you must manually define ALL table columns, always, without exception**... none will be auto-defined for you. If you want auto-defined fields, such as "createdAt", and "updatedAt" for all your models, then the recommendation is to define a BaseModel that all of your other models inherit from.
|
|
24
|
-
|
|
25
|
-
To properly understand how Mythix ORM creates table relationships/associations, it helps to view the situation from the perspective of "this model instance". Don't think of "tables", or "many" when you are thinking of the relationship. Think only of "this model instance".
|
|
30
|
+
4. In field relationships, it is required to specify a target model
|
|
31
|
+
5. All concrete fields must be manually defined on all models. There is no "automagic" or "hidden" fields in `mythix-orm`. Said another way, **you must manually define ALL table columns/model fields, always, without exception**... none will be auto-defined for you. If you want auto-defined fields, such as "createdAt", and "updatedAt" for all your models, then the recommendation is to define a BaseModel that all of your other models inherit from.
|
|
26
32
|
|
|
27
33
|
## Example #1 - Defining a 1x1 relationship
|
|
28
34
|
|
|
@@ -36,62 +42,81 @@ You could define the association like so:
|
|
|
36
42
|
class User extends Model {
|
|
37
43
|
static fields = {
|
|
38
44
|
'id': {
|
|
39
|
-
type:
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
type: Types.UUIDV4,
|
|
46
|
+
defaultValue: Types.UUIDV4.Default.UUIDV4,
|
|
47
|
+
allowNull: false,
|
|
48
|
+
primaryKey: true,
|
|
42
49
|
},
|
|
43
50
|
'firstName': {
|
|
44
|
-
type:
|
|
45
|
-
allowNull:
|
|
46
|
-
index:
|
|
51
|
+
type: Types.STRING(64),
|
|
52
|
+
allowNull: true,
|
|
53
|
+
index: true,
|
|
47
54
|
},
|
|
48
55
|
'lastName': {
|
|
49
|
-
type:
|
|
50
|
-
allowNull:
|
|
51
|
-
index:
|
|
56
|
+
type: Types.STRING(64),
|
|
57
|
+
allowNull: true,
|
|
58
|
+
index: true,
|
|
52
59
|
},
|
|
60
|
+
// Define a foreign key relationship
|
|
61
|
+
// to the Roles table, targeting the
|
|
62
|
+
// "id" column of that table.
|
|
53
63
|
'roleID': {
|
|
54
|
-
type:
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
type: Types.FOREIGN_KEY('Role:id', {
|
|
65
|
+
onDelete: 'SET NULL',
|
|
66
|
+
onUpdate: 'SET NULL',
|
|
67
|
+
}),
|
|
68
|
+
allowNull: true,
|
|
69
|
+
index: true,
|
|
57
70
|
},
|
|
58
71
|
// This defines a "virtual" field,
|
|
59
72
|
// that will be used to define
|
|
60
73
|
// methods to interact with the role
|
|
61
74
|
'role': {
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
75
|
+
// Relationships are defined by a query.
|
|
76
|
+
// Simply generate the query for the data
|
|
77
|
+
// you want the relationship to interact
|
|
78
|
+
// with, and away you go! The first model
|
|
79
|
+
// specified in the query is the "root"
|
|
80
|
+
// model, and must always match the target
|
|
81
|
+
// model specified in the first argument.
|
|
82
|
+
// "self" is always the origin model instance.
|
|
83
|
+
// So for example, when we want to get the role
|
|
84
|
+
// for this user, we would do a:
|
|
85
|
+
// `await user.getRole()`, so "self" would be
|
|
86
|
+
// "user" (the origin model instance).
|
|
65
87
|
//
|
|
66
|
-
//
|
|
67
|
-
type:
|
|
88
|
+
// target query provider
|
|
89
|
+
type: Types.Model('Role', ({ Role, self, userQuery }) => {
|
|
90
|
+
return Role.where.id.EQ(self.roleID).MERGE(userQuery);
|
|
91
|
+
}),
|
|
68
92
|
},
|
|
69
93
|
};
|
|
70
94
|
}
|
|
71
95
|
|
|
96
|
+
// Now let's not forget to define
|
|
97
|
+
// our Role model
|
|
72
98
|
class Role extends Model {
|
|
73
99
|
static fields = {
|
|
74
100
|
'id': {
|
|
75
|
-
type:
|
|
76
|
-
|
|
77
|
-
|
|
101
|
+
type: Types.UUIDV4,
|
|
102
|
+
defaultValue: Types.UUIDV4.Default.UUIDV4,
|
|
103
|
+
allowNull: false,
|
|
104
|
+
primaryKey: true,
|
|
78
105
|
},
|
|
79
106
|
'name': {
|
|
80
|
-
type:
|
|
81
|
-
allowNull:
|
|
82
|
-
index:
|
|
107
|
+
type: Types.STRING(64),
|
|
108
|
+
allowNull: false,
|
|
109
|
+
index: true,
|
|
83
110
|
},
|
|
84
111
|
// This defines a "virtual" field,
|
|
85
112
|
// that will be used to define
|
|
86
113
|
// methods to interact with the user
|
|
87
114
|
'user': {
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// Equivalent
|
|
94
|
-
// type: Types.Model('User:roleID'),
|
|
115
|
+
// Now we simply define the relationship
|
|
116
|
+
// in reverse, using a simple query.
|
|
117
|
+
type: Types.Model('User', ({ User, self, userQuery }) => {
|
|
118
|
+
return User.where.roleID.EQ(self.id).MERGE(userQuery);
|
|
119
|
+
}),
|
|
95
120
|
},
|
|
96
121
|
};
|
|
97
122
|
}
|
|
@@ -100,52 +125,121 @@ class Role extends Model {
|
|
|
100
125
|
Let's look at the `User` model `role` field first, which is of the type:
|
|
101
126
|
|
|
102
127
|
```javascript
|
|
103
|
-
type: Types.Model('Role
|
|
128
|
+
type: Types.Model('Role', ({ Role, self, userQuery }) => {
|
|
129
|
+
return Role.where.id.EQ(self.roleID).MERGE(userQuery);
|
|
130
|
+
}),
|
|
104
131
|
```
|
|
105
132
|
|
|
106
|
-
This may seem confusing at first, until you understand the pattern here. The pattern is fairly simple. The `Types.Model` is singular (`Models` on the other hand, if used, defines a one-to-many, or many-to-many relationship), so right away we know this is going to fetch a single model
|
|
133
|
+
This may seem confusing at first, until you understand the pattern here. The pattern is fairly simple. The `Types.Model` is singular (`Models` on the other hand, if used, defines a one-to-many, or many-to-many relationship), so right away we know this is going to fetch a single model. It is a 1x1 relationship. Next, we have the two arguments we provide. It helps a lot to think of these as "target model", and "query provider"... as in, "What is the target model we are interacting with, and how do we interact with it?" It also helps quite a bit to think of this from the perspective of "this model instance", instead of "this table", or "this relationship". The relationship is always from an instance of a model... a single instance (or a single row in the DB, if that helps instead).
|
|
107
134
|
|
|
108
135
|
With this in mind, the above model definitions should make a little more sense:
|
|
109
136
|
|
|
110
137
|
1. From the perspective of this specific user, what are we targeting?
|
|
111
|
-
2. We are targeting the `Role` model
|
|
112
|
-
3. Great! Now,
|
|
113
|
-
4. Oh, that is easy,
|
|
138
|
+
2. We are targeting the `Role` model (the first argument)
|
|
139
|
+
3. Great! Now, how are we interacting with this model?
|
|
140
|
+
4. Oh, that is easy, we are interacting with a `Role` model where the `Role.id` equals `user.roleID` (self.roleID).
|
|
114
141
|
|
|
115
142
|
Simple!
|
|
116
143
|
|
|
117
|
-
|
|
144
|
+
The last thing you might be wondering is "What is this `userQuery` garbage?". Good question! The last part, the `.MERGE(userQuery)` is simply merging in any user query that might have been passed along with the call. What is a `userQuery`? A user query is a query that the caller provides to any relationship method call. For example, if we made the call `await user.getRole(Role.where.name.EQ('admin'))` then the `userQuery` passed into the query provider would be `Role.where.name.EQ('admin')`. When we merge this into our "default" query, this would then fetch the user's defined role, but only if the `name` attribute of the target `Role` was equal to `"admin"`. `userQuery` generally makes more sense in the context of many-to-many relationships, but can still be quite useful for 1x1 relationships.
|
|
118
145
|
|
|
119
|
-
|
|
146
|
+
One thing to note here is that Mythix ORM expects the developer to do something with the `userQuery`. If it is not used by the developer than it will be silently discarded. Why? Because with complex relationships you might want to apply the `userQuery` to some other part of the primary query... or you actually might not want to use it at all... or something else entirely. Allowing the developer to define what happens with `userQuery` might make the code more verbose, but it also allows much more flexibility and power in how it is used.
|
|
120
147
|
|
|
121
|
-
We have:
|
|
122
148
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
Let's look at it from the `Role` perspective now.
|
|
150
|
+
|
|
151
|
+
From the perspective of the `Role` it is very similar, except the "query provider" has switched to the `User` model (as the target/root model), where the `roleID` field matches the current `Role` model instance (self) `id` attribute.
|
|
126
152
|
|
|
127
|
-
|
|
153
|
+
We have:
|
|
128
154
|
|
|
129
155
|
```javascript
|
|
130
|
-
type: Types.Model('User
|
|
156
|
+
type: Types.Model('User', ({ User, self, userQuery }) => {
|
|
157
|
+
return User.where.roleID.EQ(self.id).MERGE(userQuery);
|
|
158
|
+
}),
|
|
131
159
|
```
|
|
132
160
|
|
|
133
|
-
When no model is specified in the "target", or the "value provider" arguments, then `Self` is implied. So when we say `'id'`, what is really being specified is `Role:id`. The PK of a model is assumed as the default if no "value provider" is given. So, `Role:id`, `id`, and empty are all equivalent in this situation.
|
|
134
|
-
|
|
135
161
|
So now, let's take a look at the process here:
|
|
136
|
-
|
|
137
162
|
1. From the perspective of this specific role, what are we targeting?
|
|
138
163
|
2. We are targeting the `User` model, and its `roleID` field
|
|
139
164
|
3. Great! Now, who is providing the value to match against the `User:roleID` field?
|
|
140
|
-
4. Oh, that is easy, it is me, this specific role, and the field to pull the `User:roleID` value from is `Role:id`... as in, this very roles's primary key `id
|
|
165
|
+
4. Oh, that is easy, it is me (self), this specific role, and the field to pull the `User:roleID` value from is `Role:id`... as in, this (self) very roles's primary key `id`.
|
|
141
166
|
|
|
142
167
|
One thing that is important to know is that the model name defined in the *first* argument will be the actual model type fetched. So, from the perspective of the user, we specify `Role` as the model in the first argument, so this is the role model type that will be fetched. From the perspective of the role, we specify `User` as the model in the first argument, so this is the model type that will be fetched.
|
|
143
168
|
|
|
169
|
+
## Injected relationship methods
|
|
170
|
+
|
|
171
|
+
When we use a virtual relationship type, such as `Types.Model` or `Types.Models`, then when a model with these field types is instantiated, the model will have methods injected into it for the specified relationships. For example, above we are creating a `role` relationship to the `Role` model. When we do this, the user model will automatically get a certain set of methods injected to work with this 1x1 relationship.
|
|
172
|
+
|
|
173
|
+
For `Types.Model` (1x1 relationship), these methods are:
|
|
174
|
+
1. `queryFor`
|
|
175
|
+
2. `create`
|
|
176
|
+
3. `get`
|
|
177
|
+
4. `update`
|
|
178
|
+
5. `destroy`
|
|
179
|
+
6. `has`
|
|
180
|
+
7. `pluck`
|
|
181
|
+
|
|
182
|
+
Now what was just listed are *prefixes* for the injected methods. These methods obviously need to exist for every relationship field on the model, so these prefixes are used in combination with the field name to create these injected methods. In our specific case, `Role` (the `role` field capitalized) will be added to the end of these prefixes, giving us:
|
|
183
|
+
1. `queryForRole`
|
|
184
|
+
2. `createRole`
|
|
185
|
+
3. `getRole`
|
|
186
|
+
4. `updateRole`
|
|
187
|
+
5. `destroyRole`
|
|
188
|
+
6. `hasRole`
|
|
189
|
+
7. `pluckRole`
|
|
190
|
+
|
|
191
|
+
This is handy, because now from our user we can call any of these methods to interact with the relationship. For example, we could check to see if a user has a role simply by `await user.hasRole()`. Or we could pluck fields from the related `Role` model simply by `await user.pluckRole([ 'name' ])`.
|
|
192
|
+
|
|
193
|
+
For `Types.Models` (many-to-n relationships), these methods are:
|
|
194
|
+
1. `queryFor`
|
|
195
|
+
2. `addTo`
|
|
196
|
+
3. `get`
|
|
197
|
+
4. `set`
|
|
198
|
+
5. `removeFrom`
|
|
199
|
+
6. `destroy`
|
|
200
|
+
7. `count`
|
|
201
|
+
8. `pluck`
|
|
202
|
+
9. `has`
|
|
203
|
+
|
|
204
|
+
As with the singular `Types.Model`, the name of the field is added to the end of each... so if we had many `roles`, these would turn into:
|
|
205
|
+
1. `queryForRoles`
|
|
206
|
+
2. `addToRoles`
|
|
207
|
+
3. `getRoles`
|
|
208
|
+
4. `setRoles`
|
|
209
|
+
5. `removeFromRoles`
|
|
210
|
+
6. `destroyRoles`
|
|
211
|
+
7. `countRoles`
|
|
212
|
+
8. `pluckRoles`
|
|
213
|
+
9. `hasRoles`
|
|
214
|
+
|
|
215
|
+
Most of these injected methods are fairly self-explanatory... however, you might be wondering what the `queryFor` injected methods are used for. Simply put, these methods return the relationship query itself, and do nothing else. This can be really handy if you want to modify the query beyond what you can just do by providing a `userQuery`. For example, you could fetch the `role` relationship query, and modify it:
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
let query = await user.queryForRole();
|
|
219
|
+
|
|
220
|
+
query = query.AND.Role.name.EQ('admin');
|
|
221
|
+
|
|
222
|
+
let roles = await query.all();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Now you know! When you define a virtual relationship field, these methods will always be injected by default, for every relationship field.
|
|
226
|
+
|
|
227
|
+
Note: One other important thing to note here is that Mythix ORM will *not* overwrite your class methods. If you define a method on your `User` model called `getRole`, then Mythix ORM won't touch that method when it is injecting methods onto the model. For this reason, Mythix ORM *also* injects the same methods but prefixed with an underscore (i.e. `_queryForRole`, `_createRole`, etc...). This will allow developers to easily implement their own methods of the same name as an override, while still being able to access the injected relationship method via the underscore prefix.
|
|
228
|
+
|
|
229
|
+
i.e.:
|
|
230
|
+
```javascript
|
|
231
|
+
async getRole(...args) {
|
|
232
|
+
let role = await this._getRole(...args);
|
|
233
|
+
// do something with `role`
|
|
234
|
+
return role;
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
144
238
|
## Example #2 - Defining a One To Many relationship
|
|
145
239
|
|
|
146
240
|
Okay, so your boss comes in on Monday morning, and yells at you for implementing the previous example, because users only being able to have a single role is "just plain stupid... what were you thinking?"
|
|
147
241
|
|
|
148
|
-
Well, it is hard to argue with the boss, especially since he is correct
|
|
242
|
+
Well, it is hard to argue with the boss, especially since he is correct. Users only being able to have a single role is a just a *wee* bit limiting. So, lets update this so a user can have many roles. To do so, we will need to define a one-to-many relationship.
|
|
149
243
|
|
|
150
244
|
Let's jump right into the code this time:
|
|
151
245
|
|
|
@@ -153,46 +247,44 @@ Let's jump right into the code this time:
|
|
|
153
247
|
class User extends Model {
|
|
154
248
|
static fields = {
|
|
155
249
|
'id': {
|
|
156
|
-
type:
|
|
157
|
-
|
|
158
|
-
|
|
250
|
+
type: Types.UUIDV4,
|
|
251
|
+
defaultValue: Types.UUIDV4.Default.UUIDV4,
|
|
252
|
+
allowNull: false,
|
|
253
|
+
primaryKey: true,
|
|
159
254
|
},
|
|
160
255
|
'firstName': {
|
|
161
|
-
type:
|
|
162
|
-
allowNull:
|
|
163
|
-
index:
|
|
256
|
+
type: Types.STRING(64),
|
|
257
|
+
allowNull: true,
|
|
258
|
+
index: true,
|
|
164
259
|
},
|
|
165
260
|
'lastName': {
|
|
166
|
-
type:
|
|
167
|
-
allowNull:
|
|
168
|
-
index:
|
|
261
|
+
type: Types.STRING(64),
|
|
262
|
+
allowNull: true,
|
|
263
|
+
index: true,
|
|
169
264
|
},
|
|
170
265
|
// Let's drop this column, because now it makes
|
|
171
266
|
// no sense to have it live on the User table
|
|
267
|
+
//
|
|
172
268
|
// 'roleID': {
|
|
173
|
-
// type:
|
|
174
|
-
//
|
|
175
|
-
//
|
|
269
|
+
// type: Types.FOREIGN_KEY('Role:id', {
|
|
270
|
+
// onDelete: 'SET NULL',
|
|
271
|
+
// onUpdate: 'SET NULL',
|
|
272
|
+
// }),
|
|
273
|
+
// allowNull: true,
|
|
274
|
+
// index: true,
|
|
176
275
|
// },
|
|
177
276
|
//
|
|
178
277
|
// Let's change "role" to "roles", because
|
|
179
278
|
// now it will be plural (one to many)
|
|
180
279
|
'roles': {
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
// the Role.userID, so we can just leave off the
|
|
185
|
-
// second argument
|
|
280
|
+
// Now we simply need to change our primary
|
|
281
|
+
// query slightly. We will now be targeting
|
|
282
|
+
// a "userID" column on the Role model.
|
|
186
283
|
//
|
|
187
284
|
// Notice the use of "plural" "Models" here
|
|
188
|
-
type: Types.Models('Role
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// type: Types.Models('Role:userID', 'User:id'),
|
|
192
|
-
//
|
|
193
|
-
// or:
|
|
194
|
-
//
|
|
195
|
-
// type: Types.Models('Role:userID', 'id'),
|
|
285
|
+
type: Types.Models('Role', ({ Role, self, userQuery }) => {
|
|
286
|
+
return Role.where.userID.EQ(self.id).MERGE(userQuery);
|
|
287
|
+
}),
|
|
196
288
|
},
|
|
197
289
|
};
|
|
198
290
|
}
|
|
@@ -200,38 +292,45 @@ class User extends Model {
|
|
|
200
292
|
class Role extends Model {
|
|
201
293
|
static fields = {
|
|
202
294
|
'id': {
|
|
203
|
-
type:
|
|
204
|
-
|
|
205
|
-
|
|
295
|
+
type: Types.UUIDV4,
|
|
296
|
+
defaultValue: Types.UUIDV4.Default.UUIDV4,
|
|
297
|
+
allowNull: false,
|
|
298
|
+
primaryKey: true,
|
|
206
299
|
},
|
|
207
300
|
'name': {
|
|
208
|
-
type:
|
|
209
|
-
allowNull:
|
|
210
|
-
index:
|
|
301
|
+
type: Types.STRING(64),
|
|
302
|
+
allowNull: false,
|
|
303
|
+
index: true,
|
|
211
304
|
},
|
|
212
|
-
//
|
|
213
|
-
//
|
|
305
|
+
// Define a foreign key relationship
|
|
306
|
+
// to the User table, targeting the
|
|
307
|
+
// "id" column of that table.
|
|
214
308
|
'userID': {
|
|
215
|
-
type:
|
|
216
|
-
|
|
217
|
-
|
|
309
|
+
type: Types.FOREIGN_KEY('User:id', {
|
|
310
|
+
// Before we didn't want to delete
|
|
311
|
+
// a User if we deleted a role, so
|
|
312
|
+
// we set these values to "SET NULL".
|
|
313
|
+
// Now, if we delete a User, it would
|
|
314
|
+
// make sense that we would want to
|
|
315
|
+
// delete all the User's roles... so
|
|
316
|
+
// this time we "CASCADE".
|
|
317
|
+
onDelete: 'CASCADE',
|
|
318
|
+
onUpdate: 'CASCADE',
|
|
319
|
+
}),
|
|
320
|
+
allowNull: false,
|
|
321
|
+
index: true,
|
|
218
322
|
},
|
|
219
323
|
// This is still correctly named, as
|
|
220
|
-
// each role will only link back
|
|
221
|
-
// a single user
|
|
324
|
+
// each role will still only link back
|
|
325
|
+
// to a single user
|
|
222
326
|
'user': {
|
|
223
327
|
// Now we update this, flipping the
|
|
224
328
|
// relationship. If we store a "userID"
|
|
225
329
|
// on the Role table, then a user can
|
|
226
330
|
// have many roles.
|
|
227
|
-
type:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// type: Types.Model('User:id', 'userID'),
|
|
231
|
-
//
|
|
232
|
-
// or:
|
|
233
|
-
//
|
|
234
|
-
// type: Types.Model('User:id', 'Role:userID'),
|
|
331
|
+
type: Types.Model('User', ({ User, self, userQuery }) => {
|
|
332
|
+
return User.where.id.EQ(self.userID).MERGE(userQuery);
|
|
333
|
+
}),
|
|
235
334
|
},
|
|
236
335
|
};
|
|
237
336
|
}
|
|
@@ -239,87 +338,41 @@ class Role extends Model {
|
|
|
239
338
|
|
|
240
339
|
Hopefully this makes sense. We simply removed `roleID` from the User model, and moved it over to instead be `userID` on the other side of the relationship. Now a user can have many roles, because instead of each user only defining a single `roleID`, now Roles define a `userID`, and since many roles can define the same `userID`, a user can have many roles.
|
|
241
340
|
|
|
242
|
-
##
|
|
341
|
+
## A quick aside on Foreign Keys
|
|
243
342
|
|
|
244
|
-
Great! So far so good. Hopefully my reader is still following me.
|
|
343
|
+
Great! So far so good. Hopefully my reader is still following me. If you don't know what "foreign keys" are, or don't understand the concept behind them, I suggest you go take a moment to read about them. In short, "foreign keys" simultaneously define an index, and one or more constraints. This means the database will disallow certain things. For example, if you specify that a relationship between a user and a role **must** exist, and **must** be valid, then the database will throw an error if you try to add a role without a user.
|
|
245
344
|
|
|
246
|
-
|
|
345
|
+
The `Types.FOREIGN_KEY` type allows us to define a foreign key relationship. The first argument is the target model and field. The second argument are simply the "options" for the foreign key.
|
|
247
346
|
|
|
248
|
-
|
|
347
|
+
Using the `FOREIGN_KEY` where appropriate is important. It is the **only** way that Mythix ORM knows your models are related. Mythix ORM uses this field type to update related attributes on models during load and store. Without using foreign keys, Mythix ORM will not know that your models are related, and so you might struggle when attempting to store or load related models. For example, because we defined `FOREIGN_KEY` types above for our `Role` model, linking to the `User` model `id` field, when Mythix ORM loads a `Role` model from the database, it will automatically know that it needs to assign the `userID` attribute of the `Role` model to the user's `id` field. This will happen even if the `userID` column is not loaded from the database.
|
|
249
348
|
|
|
250
|
-
|
|
251
|
-
class User extends Model {
|
|
252
|
-
static fields = {
|
|
253
|
-
'id': {
|
|
254
|
-
type: Types.UUIDV4,
|
|
255
|
-
allowNull: false,
|
|
256
|
-
primaryKey: true,
|
|
257
|
-
},
|
|
258
|
-
'firstName': {
|
|
259
|
-
type: Types.STRING(64),
|
|
260
|
-
allowNull: true,
|
|
261
|
-
index: true,
|
|
262
|
-
},
|
|
263
|
-
'lastName': {
|
|
264
|
-
type: Types.STRING(64),
|
|
265
|
-
allowNull: true,
|
|
266
|
-
index: true,
|
|
267
|
-
},
|
|
268
|
-
'roles': {
|
|
269
|
-
type: Types.Models('Role:userID'),
|
|
270
|
-
},
|
|
271
|
-
};
|
|
272
|
-
}
|
|
349
|
+
The other thing `FOREIGN_KEY` types open up to you as the developer is easier bulk-model creation. For example, the following is completely valid, but **only** when using the `FOREIGN_KEY` type, so that Mythix ORM knows the models are related: `await Role.create({ name: 'admin', user: { firstName: 'Bob', lastName: 'Brown' } });`. What this is doing (even though it is strange, and likely doesn't reflect a real-world example), is creating the user *at the same time* that it creates the role. Mythix ORM is smart enough to understand the relationships, and it knows that in order to create a `Role` model it must first have a `userID`, so it will create and store the specified user first, and then create the role requested using the created user's `id`. It knows that `user: {some value}` is a `User`, because of the target model defined on that virtual field.
|
|
273
350
|
|
|
274
|
-
|
|
275
|
-
static fields = {
|
|
276
|
-
'id': {
|
|
277
|
-
type: Types.UUIDV4,
|
|
278
|
-
allowNull: false,
|
|
279
|
-
primaryKey: true,
|
|
280
|
-
},
|
|
281
|
-
'name': {
|
|
282
|
-
type: Types.STRING(64),
|
|
283
|
-
allowNull: false,
|
|
284
|
-
index: true,
|
|
285
|
-
},
|
|
286
|
-
'userID': {
|
|
287
|
-
type: Types.ForeignKey('User:id', {
|
|
288
|
-
// if User is deleted, also delete all roles belonging to the user
|
|
289
|
-
onDelete: 'CASCADE',
|
|
290
|
-
}),
|
|
351
|
+
If the provided value to the role's `user` attribute was instead an already persisted `User` model instance, than Mythix ORM would skip storing the model, and simply pull its `id` for the `userID` attribute of the role.
|
|
291
352
|
|
|
292
|
-
|
|
293
|
-
allowNull: false,
|
|
353
|
+
**Important note:** Mythix ORM ignores many-to-n relationships by default with these types of operations. So you *can not* do the reverse, such as: `await User.create({ ..., roles: [ { name: 'admin' } ] })`. This **will not** work. Mythix ORM doesn't know what is desired in this case. What if the user already had some roles? Would they be removed? What if we wanted to just add instead? Because Mythix ORM can not know what you want to do with many-to-n relationships in a case like this, it simply isn't supported. With many-to-n relationships you must be explicit, and generally will use the relationship methods injected by the type itself. For example, instead of the above, you would have to:
|
|
294
354
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
},
|
|
299
|
-
'user': {
|
|
300
|
-
type: Types.Model('User', 'userID'),
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
}
|
|
355
|
+
```javascript
|
|
356
|
+
let user = await User.create({ firstName: 'Bob', lastName: 'Brown' });
|
|
357
|
+
let role = await user.addToRoles({ name: 'admin' });
|
|
304
358
|
```
|
|
305
359
|
|
|
306
|
-
|
|
360
|
+
This *will* work, because Mythix ORM now understands that you want to *add* to the many-to-n model set.
|
|
307
361
|
|
|
308
|
-
## Example #
|
|
362
|
+
## Example #3 - Using a "through" table in relationships
|
|
309
363
|
|
|
310
|
-
So far we have just defined relationships between two different tables, our `User` and `Role` tables. What if we instead want to define more that two tables in our relationship? What if, for example, our boss comes back to us on Friday morning and says "Dang, Shayla, okay, well, this situation is slightly better. Now users can have more than one role, but we need to define role information
|
|
364
|
+
So far we have just defined relationships between two different tables, our `User` and `Role` tables. What if we instead want to define more that two tables in our relationship? What if, for example, our boss comes back to us on Friday morning and says "Dang, Shayla, okay, well, this situation is slightly better. Now users can have more than one role, but we need to define role information *on the relationship* itself."
|
|
311
365
|
|
|
312
|
-
Back to work! Now the boss wants extra information defined
|
|
366
|
+
Back to work! Now the boss wants extra information defined *on the relationship*, not on users, and not on roles, but rather on the relationship between the two. The boss wants us to define if a user, with a specific role, is able to use the front-door of the office, the back-door of the office, or both.
|
|
313
367
|
|
|
314
368
|
Okay, strange request boss... but then again, bosses generally don't do the thinking very well, which is why they hired you, right? Great! Let's get r' done.
|
|
315
369
|
|
|
316
370
|
For this to work, we now need to define a third table. We will call this table `UserRole`. This will define the user id, the role id, and which door the user is allowed to use. This will also mean that we need to remove the `userID` from the `Role` table.
|
|
317
371
|
|
|
318
372
|
So to sum up, we need to do the following:
|
|
319
|
-
|
|
320
373
|
1. Drop the `userID` from the `Role` table
|
|
321
|
-
2. Create a new table called `UserRole`
|
|
322
|
-
3. Adjust the relationships between the
|
|
374
|
+
2. Create a new table/model called `UserRole`
|
|
375
|
+
3. Adjust the relationships between the models
|
|
323
376
|
|
|
324
377
|
Let's dig in:
|
|
325
378
|
|
|
@@ -327,45 +380,36 @@ Let's dig in:
|
|
|
327
380
|
class User extends Model {
|
|
328
381
|
static fields = {
|
|
329
382
|
'id': {
|
|
330
|
-
type:
|
|
331
|
-
|
|
332
|
-
|
|
383
|
+
type: Types.UUIDV4,
|
|
384
|
+
defaultValue: Types.UUIDV4.Default.UUIDV4,
|
|
385
|
+
allowNull: false,
|
|
386
|
+
primaryKey: true,
|
|
333
387
|
},
|
|
334
388
|
'firstName': {
|
|
335
|
-
type:
|
|
336
|
-
allowNull:
|
|
337
|
-
index:
|
|
389
|
+
type: Types.STRING(64),
|
|
390
|
+
allowNull: true,
|
|
391
|
+
index: true,
|
|
338
392
|
},
|
|
339
393
|
'lastName': {
|
|
340
|
-
type:
|
|
341
|
-
allowNull:
|
|
342
|
-
index:
|
|
394
|
+
type: Types.STRING(64),
|
|
395
|
+
allowNull: true,
|
|
396
|
+
index: true,
|
|
343
397
|
},
|
|
344
398
|
'roles': {
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
// necessary to join the tables.
|
|
348
|
-
//
|
|
349
|
-
// Mythix ORM is smart enough to follow all fields
|
|
350
|
-
// until it can build the full relationship.
|
|
351
|
-
// So Mythix ORM will first lookup `UserRole:role`,
|
|
352
|
-
// find that it is a virtual field that targets `roleID`,
|
|
353
|
-
// which it will then look-up, and find that this is a
|
|
354
|
-
// foreign key that points to `Role.id`.
|
|
355
|
-
// So now it knows how to get to the role.
|
|
399
|
+
// Now we update our primary query to
|
|
400
|
+
// go through the UserRole table.
|
|
356
401
|
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
),
|
|
402
|
+
// Note: Notice how we are .EQ against
|
|
403
|
+
// a query that has no conditions. We
|
|
404
|
+
// ask for `Role.where.id.EQ(UserRole.where.id)`.
|
|
405
|
+
// This is a table join. When you use any
|
|
406
|
+
// conditional operator on a FIELD from
|
|
407
|
+
// another table, without any conditions of
|
|
408
|
+
// its own, then Mythix ORM translates this
|
|
409
|
+
// as a request to join tables.
|
|
410
|
+
type: Types.Models('Role', ({ Role, UserRole, self, userQuery }) => {
|
|
411
|
+
return Role.where.id.EQ(UserRole.where.roleID).AND.UserRole.userID.EQ(self.id).MERGE(userQuery);
|
|
412
|
+
}),
|
|
369
413
|
},
|
|
370
414
|
};
|
|
371
415
|
}
|
|
@@ -373,22 +417,20 @@ class User extends Model {
|
|
|
373
417
|
class Role extends Model {
|
|
374
418
|
static fields = {
|
|
375
419
|
'id': {
|
|
376
|
-
type:
|
|
377
|
-
|
|
378
|
-
|
|
420
|
+
type: Types.UUIDV4,
|
|
421
|
+
defaultValue: Types.UUIDV4.Default.UUIDV4,
|
|
422
|
+
allowNull: false,
|
|
423
|
+
primaryKey: true,
|
|
379
424
|
},
|
|
380
425
|
'name': {
|
|
381
|
-
type:
|
|
382
|
-
allowNull:
|
|
383
|
-
index:
|
|
426
|
+
type: Types.STRING(64),
|
|
427
|
+
allowNull: false,
|
|
428
|
+
index: true,
|
|
384
429
|
},
|
|
385
430
|
'user': {
|
|
386
|
-
type:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// Value provider (will be fully resolved)
|
|
390
|
-
'UserRole:role',
|
|
391
|
-
),
|
|
431
|
+
type: Types.Model('User', ({ User, UserRole, self, userQuery }) => {
|
|
432
|
+
return User.where.id.EQ(UserRole.where.userID).AND.UserRole.roleID.EQ(self.id).MERGE(userQuery);
|
|
433
|
+
}),
|
|
392
434
|
},
|
|
393
435
|
};
|
|
394
436
|
}
|
|
@@ -396,87 +438,102 @@ class Role extends Model {
|
|
|
396
438
|
class UserRole extends Model {
|
|
397
439
|
static fields = {
|
|
398
440
|
'id': {
|
|
399
|
-
type:
|
|
400
|
-
|
|
401
|
-
|
|
441
|
+
type: Types.UUIDV4,
|
|
442
|
+
defaultValue: Types.UUIDV4.Default.UUIDV4,
|
|
443
|
+
allowNull: false,
|
|
444
|
+
primaryKey: true,
|
|
402
445
|
},
|
|
403
446
|
// The new data point our boss wanted us to add
|
|
404
447
|
'doorUsage': {
|
|
405
|
-
type:
|
|
406
|
-
allowNull:
|
|
407
|
-
index:
|
|
448
|
+
type: Types.STRING(16),
|
|
449
|
+
allowNull: false,
|
|
450
|
+
index: true,
|
|
408
451
|
},
|
|
409
452
|
'userID': {
|
|
410
|
-
type:
|
|
411
|
-
|
|
412
|
-
|
|
453
|
+
type: Types.FOREIGN_KEY('User:id', {
|
|
454
|
+
onDelete: 'CASCADE',
|
|
455
|
+
onUpdate: 'CASCADE',
|
|
413
456
|
}),
|
|
414
|
-
allowNull:
|
|
457
|
+
allowNull: false,
|
|
458
|
+
index: true,
|
|
415
459
|
},
|
|
416
460
|
'roleID': {
|
|
417
|
-
type:
|
|
418
|
-
|
|
419
|
-
|
|
461
|
+
type: Types.FOREIGN_KEY('Role:id', {
|
|
462
|
+
onDelete: 'CASCADE',
|
|
463
|
+
onUpdate: 'CASCADE',
|
|
420
464
|
}),
|
|
421
|
-
allowNull:
|
|
465
|
+
allowNull: false,
|
|
466
|
+
index: true,
|
|
422
467
|
},
|
|
468
|
+
// These aren't needed, but they might be
|
|
469
|
+
// nice to have if we already have an instance
|
|
470
|
+
// of a UserRole model, and want to interact
|
|
471
|
+
// with the user or role of that instance.
|
|
423
472
|
'role': {
|
|
424
|
-
|
|
425
|
-
|
|
473
|
+
type: Types.Model('Role', ({ Role, self, userQuery }) => {
|
|
474
|
+
return Role.where.id.EQ(self.roleID).MERGE(userQuery);
|
|
475
|
+
})
|
|
426
476
|
},
|
|
427
477
|
'user': {
|
|
428
|
-
|
|
429
|
-
|
|
478
|
+
type: Types.Model('User', ({ User, self, userQuery }) => {
|
|
479
|
+
return User.where.id.EQ(self.userID).MERGE(userQuery);
|
|
480
|
+
}),
|
|
430
481
|
},
|
|
431
482
|
};
|
|
432
483
|
}
|
|
433
484
|
```
|
|
434
485
|
|
|
435
|
-
This is fantastic! But... we have one problem. This through-table relationship will indeed do a three-way join to collect the information requested from all tables, but what about that new `doorUsage` field we added to the `UserRole` table? Nowhere have we specified where that
|
|
486
|
+
This is fantastic! But... we have one problem. This through-table relationship will indeed do a three-way join to collect the information requested from all tables, but what about that new `doorUsage` field we added to the `UserRole` table? Nowhere have we specified where that can be found... Mythix ORM could just inject it onto `Role` models on load... but that doesn't make sense, because the user could have multiple roles, and each role link could have a different `doorUsage` value. Also, what if the `Role` model itself had a `doorUsage` field? That would not be ideal... Instead, it would be better if we could simply access the `doorUsage` attribute on any loaded through-table relationship.
|
|
436
487
|
|
|
437
|
-
|
|
488
|
+
When Mythix ORM loads a model through a relationship like this, it will by default *only* load the root/target model. This default behavior is deliberate to improve the performance and efficiency of the library. In order to also fetch the related models at the same time, you must directly specify a projection that includes the related model. When you do this, Mythix ORM will *then* include the specified related models while loading data.
|
|
489
|
+
|
|
490
|
+
Because we *want* to access the `doorUsage` attribute on the through-table model (`UserRole`), in the examples below you will see we use a `.PROJECT('+UserRole:doorUsage')`. This informs Mythix ORM that we want to add (`+`) the field `UserRole:doorUsage` to the query projection, which also means that Mythix ORM will load the `UserRole` model and assign it to our role.
|
|
438
491
|
|
|
439
492
|
```javascript
|
|
440
|
-
|
|
493
|
+
// To get the roles along with their related
|
|
494
|
+
// UserRole model, we PROJECT on the UserRole
|
|
495
|
+
// model `doorUsage` field. This will ensure
|
|
496
|
+
// that Mythix ORM includes the related models
|
|
497
|
+
// (even though those models will only be
|
|
498
|
+
// partially loaded, and will only contain the
|
|
499
|
+
// `doorUsage` field).
|
|
500
|
+
let roles = await thisUser.getRoles(Role.where.PROJECT('+UserRole:doorUsage'));
|
|
441
501
|
```
|
|
442
502
|
|
|
443
|
-
|
|
503
|
+
When we load the relationship this way, every `role` in the return `roles` array will also have a `UserRoles` key, containing the related `UserRole` models from the through table relationship.
|
|
444
504
|
|
|
445
|
-
|
|
446
|
-
let roles = await thisUser.getRoles();
|
|
447
|
-
// roles[0].userRole = UserRole { userID, roleID, id, doorUsage }
|
|
448
|
-
```
|
|
505
|
+
Note: Because we only projected on the `UserRole:doorUsage` field, when you access one of the related `UserRole` models, the models will be incomplete, and will only have a `doorUsage` field set on them. This is fine though, because that is the only data point we need. Do **not** try and directly store these models back to the database however, because 1) without their primary key (`id`) any attempt to save will immediately fail, and 2) with missing attributes, even if you were able to get the model to save, you might end up with corruption (missing field values) in the database.
|
|
449
506
|
|
|
450
|
-
|
|
507
|
+
Now, we can access the related model and find the `doorUsage` attribute we are looking for.
|
|
451
508
|
|
|
452
509
|
```javascript
|
|
453
|
-
let
|
|
454
|
-
|
|
510
|
+
let roles = await loadedUserInstance.getRoles(Role.where.name.EQ('restricted-door-usage').PROJECT('+UserRole:doorUsage'));
|
|
511
|
+
let doorUsage = roles[0].UserRoles[0].doorUsage;
|
|
455
512
|
```
|
|
456
513
|
|
|
457
|
-
|
|
458
|
-
If this was a many-to-many relationship, then `userRole` would be added to **both-sides** of the relation, being added to both loaded `User` models, and also to loaded `Role` models.*
|
|
459
|
-
|
|
460
|
-
Great! Now we can easily access this and get the job done as the boss asked:
|
|
514
|
+
Great! Now we can easily access this attribute and get the job done as the boss asked:
|
|
461
515
|
|
|
462
516
|
```javascript
|
|
463
|
-
let
|
|
464
|
-
let
|
|
517
|
+
let user = await User.where.id.EQ(userIDFromDoorRFIDScanner).first();
|
|
518
|
+
let roles = await user.getRoles(Role.where.name.EQ('restricted-door-usage').PROJECT('+UserRole:doorUsage'));
|
|
519
|
+
let doorUsage = roles[0].UserRoles[0].doorUsage;
|
|
465
520
|
|
|
466
|
-
if (
|
|
521
|
+
if (doorUsage === 'back') {
|
|
467
522
|
// User is only allowed to use the back door
|
|
468
|
-
} else if (
|
|
523
|
+
} else if (doorUsage === 'front') {
|
|
469
524
|
// User is only allowed to use the front door
|
|
470
525
|
} else {
|
|
471
|
-
// User can use either door... the boss
|
|
472
|
-
// ensured that this
|
|
526
|
+
// User can use either door... the boss
|
|
527
|
+
// has conveniently ensured that this
|
|
528
|
+
// case applies to him...
|
|
473
529
|
}
|
|
474
530
|
```
|
|
475
531
|
|
|
476
532
|
## Final Notes
|
|
477
533
|
|
|
478
534
|
1. I would like to bring it to the attention of the reader that concrete types are defined all *UPPERCASE*, whereas virtual types are defined as *CamelCase*.
|
|
479
|
-
2. "But, what if I am using two, three, or more through-tables?" you ask. Well, you are in luck! Nothing described above changes. You simply
|
|
480
|
-
3. Mythix forces you to manually define all table fields. Yes, this is extra overhead, but it comes with the benefit of not needing to painstakingly manually define all *relationships*. This design pattern was also decided upon so that there aren't any table columns that are "hidden" from
|
|
535
|
+
2. "But, what if I am using two, three, or more through-tables?" you ask. Well, you are in luck! Nothing described above changes. You simply define a more complex primary query using the query provider, and Mythix ORM will be smart enough to recursively walk the relationships in the query, join as many tables as it needs to to get the job done, and do the right thing.
|
|
536
|
+
3. Mythix forces you to manually define all table/model fields. Yes, this is extra overhead, but it comes with the benefit of not needing to painstakingly manually define all *relationships*. This design pattern was also decided upon so that there aren't any table columns that are ambiguously "hidden" from developers. By forcing the user to always define all columns/fields manually, it simplifies seeing what fields exist on the table, removes down-stream dependencies, and prevents the user from needing to go look-up documentation to understand how things are working and why. Feel free to write your own helper methods that will automatically inject fields into your schema for you!
|
|
537
|
+
4. When pulling related models, Mythix ORM will always put them in a plural {model name} key... i.e. if you noticed above, `UserRole` models that were fetched were placed into `.UserRoles`. This is true for all relationship operations. If you opt-in to loading other relationships during an operation, the related models will always be placed on the loaded model instances, under their plural name (always as an array of models).
|
|
481
538
|
|
|
482
539
|
Happy coding!
|