-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FeatureRequest] Could there be a proc that automatically fetches all "Many" entries of a "One-to-many" relationship ? #127
Comments
I've made some pretty decent headway in this regard. You CAN have a function that fetches the "many" side on a one-to-many relationship, assuming you have the "one" instance. You can do it with these 3 generic procs: (Be careful: ##[This function fetches the name of a field if it is an FK-Field and points to the Model `targetType`]##
proc getRelatedFieldName[M: Model, O:Model](targetType: typedesc[O], sourceType: typedesc[M]): Option[string] =
let source = sourceType()
for sourceFieldName, sourceFieldValue in source[].fieldPairs:
#Handles case where field is an int64 with fk pragma
when sourceFieldValue.hasCustomPragma(fk):
when O.table() == sourceFieldValue.getCustomPragmaVal(fk).table():
return some(sourceFieldName)
#Handles case where field is a Model type
when sourceFieldValue is Model:
when O.table() == sourceFieldValue.type().table():
return some(sourceFieldName)
#Handles case where field is a Option[Model] type
when sourceFieldValue is Option:
when sourceFieldValue.get() is Model:
when O.table() == genericParams(sourceFieldValue.type()).get(0).table():
return some(sourceFieldName)
return none(string)
##[Just a wrapper around `getRelatedFieldName` to deal with the option that you might not find a field ]##
proc getForeignKeyFieldNameOn*[M: Model, O:Model](oneModel: typedesc[O], manyModel: typedesc[M]): string =
let fieldOption: Option[string] = getRelatedFieldName(oneModel, manyModel)
if fieldOption.isNone():
let errorMsg = "Tried getting foreign key field from model '" & name(manyModel) & "' to model '" & name(oneModel) & "' but there is no such field!"
raise newException(FieldDefect, errorMsg)
return fieldOption.get()
##[ This generic function takes the instances whose related Models of type M you want to fetch ]##
proc getManyFromOne*[O: Model, M: Model](oneEntry: O, relatedManyType: typedesc[M]): seq[M] =
mixin newModel
let db: DbConn = getDatabaseConnection()
var targetEntries: seq[relatedManyType] = @[newModel(relatedManyType)]
let oneTableName: string = oneEntry.type().table()
var foreignKeyFieldName: string = oneEntry.type().getForeignKeyFieldNameOn(relatedManyType)
let manyTableName: string = relatedManyType.table()
let sqlCondition: string = manyTableName & "." & foreignKeyFieldName & " = ?"
db.select(targetEntries, sqlCondition, oneEntry.id)
result = targetEntries Some minimal manual testing code for the above functions as I haven't yet bothered to write tests type
A = ref object of Model # <-- has implicit tableName "A"
name: string
AC {.tableName: "A".} = ref object of Model
myothername: string
name: string
B = ref object of Model # <-- has implicit tableName "B"
name: string
myA: Option[A]
D = ref object of Model
myothernameid: string
myDA: A
E = ref object of Model
myotherbool: bool
myEA {.fk: A.}: int64
echo A.getRelatedFieldName(B) # some("myA")
echo AC.getRelatedFieldName(B) # some("myA")
echo A.getRelatedFieldName(D) # some("myDA")
echo AC.getRelatedFieldName(D) # some("myDA")
echo A.getRelatedFieldName(E) # some("myEA")
echo AC.getRelatedFieldName(E) # some("myEA") |
In fact, with the help of From there it'll do the query to the database on the join Table and automatically also fetch the data for the A macro I got essentially gifted from ElegantBeef then helps generically "unpack" that macro unpackFromJoinModel*[T: Model](mySeq: seq[T], field: static string): untyped =
newCall(bindSym"mapIt", mySeq, nnkDotExpr.newTree(ident"it", ident field))
proc getManyToMany*[MS: Model, J: Model](queryStartEntry: MS, joinModel: typedesc[J], foreignKeyField: static string): seq[untyped] =
mixin newModel
let db = getDatabaseConnection()
var joinModelEntries: seq[joinModel] = @[]
joinModelEntries.add(newModel(joinModel))
let fkColumnFromJoinToManyStart: string = queryStartEntry.type().getForeignKeyFieldNameOn(joinModel)
let joinTableName = joinModel.table()
let sqlCondition: string = joinTableName & '.' & fkColumnFromJoinToManyStart & " = ?"
db.select(joinModelEntries, sqlCondition, queryStartEntry.id)
let manyEntries = unpackFromJoinModel[J](joinModelEntries, foreignKeyField)
result = manyEntries
For the most part to get a feel for the macro, here some example code (Take the types of A and D from the above comment): macro mapModel[T: Model](mySeq: seq[T], field: static string): untyped =
newCall(bindSym"mapIt", mySeq, nnkDotExpr.newTree(ident"it", ident field))
var myDSeq: seq[D] = @[]
let anA: A = A(name: "this is an A")
myDSeq.add(D(myothernameid: "la", myDA: anA))
myDSeq.add(D(myothernameid: "le", myDA: anA))
echo %*myDSeq
let myASeq: seq[A] = mapModel(myDSeq, "myDA")
echo %*myASeq |
Looks cool! A few motes before I'll ask you to submit a PR:
Anyway, I suggest you submit a PR and we'll discuss the code in the сщььуте. |
Ah, that was more the code I had so far for my side-project (It's part of my "genericArticleRepository"), I hadn't yet taken the effort to rewrite it more to suit norm's philosophy since I wanted to first give a gist of how I tackled the problem for my side-project before I did a rewrite for norm, in case you had already plans for going in a different direction. The key idea was, use The
I tried these approaches first. I only managed to make templates work if you call them directly and you have to give them the field, can't do that in string form or anything. As part of a generic function, it did not work, as that would mean the generic function would need to receive a field as some parameter somehow and pass that on to the template. I'm not sure that's possible. Here how far I got with templates. import std/[options, typetraits, json, sequtils]
import norm/[model, pragmas]
import macros
type
A = ref object of Model
name: string
D = ref object of Model
myothernameid: string
myDA: A
template mapModel[T: Model](mySeq: seq[T], field: untyped): seq[untyped] = mySeq.mapIt(it.field)
var myDSeq: seq[D] = @[]
let anA: A = A(name: "this is an A")
myDSeq.add(D(myothernameid: "la", myDA: anA))
myDSeq.add(D(myothernameid: "le", myDA: anA))
myDSeq.add(D(myothernameid: "li", myDA: anA))
myDSeq.add(D(myothernameid: "lo", myDA: anA))
myDSeq.add(D(myothernameid: "lu", myDA: anA))
echo %*myDSeq
let myASeq: seq[A] = mapModel(myDSeq, myDA)
echo %*myASeq Notice how |
As a sidenote, if you have any inspiration for better names than "getManyToMany" or "getManyFromOne" I am very eager to hear about them. I'm none too happy with those names, they were just the best name that I could think of. |
I played around a bit with this and couldn't find a way to make this work. The important thing is, that proc getRelatedFieldNameOn*[T: Model, M: Model](targetModel: T, sourceModel: M): string =
for sourceFieldName, sourceFieldValue in sourceModel[].fieldPairs:
#Handles case where field is an int64 with fk pragma
when sourceFieldValue.hasCustomPragma(fk):
when targetModel.type().table() == sourceFieldValue.getCustomPragmaVal(fk).table():
return sourceFieldName
#Handles case where field is a Model type
when sourceFieldValue.isModel():
when targetModel.type().table() == sourceFieldValue.type().table():
return sourceFieldName
#Handles case where field is a Option[Model] type
when sourceFieldValue is Option:
when sourceFieldValue.get().isModel():
when targetModel.type().table() == genericParams(sourceFieldValue.type()).get(0).table():
return sourceFieldName
raise newException(
FieldDefect,
"Tried getting foreign key field from model '" & name(sourceModel.type()) & "' to model '" & name(targetModel.type()) & "' but there is no such field!"
) I receive compile errors like this:
I can rewrite the above to use |
Alright, I've got my core functions hammered down and got them tested under sqlite. There's one thing I'm not yet quite sure on, is how to deal regarding organizing said code. If you wish to look at the code, here's the branch of it in my repo: https://github.com/PhilippMDoerner/norm/tree/manytoxfetching The entire PR that'll be made for this consists of Here's the procs: # Couldn't find a way to make getRelatedFieldNameOn work at compile time without solely using types instead of objects
#However, as this never accesses and field values, this proc does get away with it, even if there are DateTime fields
proc getRelatedFieldNameOn*[T: Model, M: Model](targetModel: typedesc[T], sourceModel: typedesc[M]): string {.raises: [FieldDefect].} =
for sourceFieldName, sourceFieldValue in sourceModel()[].fieldPairs:
#Handles case where field is an int64 with fk pragma
when sourceFieldValue.hasCustomPragma(fk):
when T.table() == sourceFieldValue.getCustomPragmaVal(fk).table():
return sourceFieldName
#Handles case where field is a Model type
when sourceFieldValue is Model:
when T.table() == sourceFieldValue.type().table():
return sourceFieldName
#Handles case where field is a Option[Model] type
when sourceFieldValue is Option:
when sourceFieldValue.get() is Model:
when T.table() == genericParams(sourceFieldValue.type()).get(0).table():
return sourceFieldName
raise newException(
FieldDefect,
"Tried getting foreign key field from model '" & name(sourceModel.type()) & "' to model '" & name(targetModel.type()) & "' but there is no such field!"
) # One-to-Many Fetching
proc selectOneToMany*[O: Model, M: Model](dbConn; oneEntry: O, relatedEntries: var seq[M]) =
const foreignKeyFieldName: string = O.getRelatedFieldNameOn(M)
const manyTableName: string = M.table()
const sqlCondition: string = "$#.$# = ?" % [manyTableName, foreignKeyFieldName]
dbConn.select(relatedEntries, sqlCondition, oneEntry.id) # Many-to-Many Fetching
macro unpackFromJoinModel*[T: Model](mySeq: seq[T], field: static string): untyped =
newCall(bindSym"mapIt", mySeq, nnkDotExpr.newTree(ident"it", ident field))
proc selectManyToMany*[M1: Model, J: Model, M2: Model](dbConn; queryStartEntry: M1, joinModelEntries: var seq[J], queryEndEntries: var seq[M2]) =
const fkColumnFromJoinToManyStart: string = M1.getRelatedFieldNameOn(J)
const joinTableName = J.table()
const sqlCondition: string = "$#.$# = ?" % [joinTableName, fkColumnFromJoinToManyStart]
dbConn.select(joinModelEntries, sqlCondition, queryStartEntry.id)
const fkColumnFromJoinToManyEnd: string = M2.getRelatedFieldNameOn(J)
let unpackedEntries: seq[M2] = unpackFromJoinModel(joinModelEntries, fkColumnFromJoinToManyEnd)
queryEndEntries = unpackedEntries
@moigagoo A particular question I'd have at you: |
…x_procs #127 Adding basic procs that automatically fetches all "Many" entries of a "One-to-many" relationship
PR for this issue has been merged. |
This was written back in 2019 as an off-hand comment, but I think this is still a really good idea.... particularly because I'm running into a situation where it'd be really useful 😄
I'd only specify that I think that the "one"-side of the parameters should be an instance, not just the type itself, so that you automatically have the ID of the one-side to query the many side with.
This could be a proc:
proc getMany[T: Model](one: Model, many: typedesc[T]): seq[T] =
The proc could scan the provided type T for the FK field to "one" and build an SQL query on T's table accordingly.
Would this be something easy to implement?
The text was updated successfully, but these errors were encountered: