Save Helpers #83
Replies: 5 comments 1 reply
-
This is what I've come up with so far. let saveUsers(cf: ContextFactory) (users: User seq) =
async {
use ctx = cf.OpenContext()
let! existingRows =
selectAsync' (Shared ctx) {
for u in dbo.Users do
select u
}
let incomingRows : dbo.Users list =
incoming
|> List.map (fun u -> { Id = u.Id; FName = u.FName; LName = u.LName })
do! Diff.Compare(incomingRows, existingRows , getPK = _.Id)
.Add(fun added->
insert {
into dbo.Users
entities added
}
)
.Change(fun changed ->
update {
for row in dbo.Users do
set row.FName changed.FName
set row.LName changed.LName
where (row.Id = changed.Id)
}
.Remove(fun removed ->
delete {
for row in dbo.Users do
where (row.Id = removed.Id)
}
)
.SaveAsync(ctx) |> Async.Ignore
}
Supports:
|
Beta Was this translation helpful? Give feedback.
-
Our workflow is pretty different. I'll share a bit about our use-case not because I think SqlHydra should support any of it, but if any of the ideas in here are applicable to our use-case that's maybe an indication that it's got some general application in other areas.
So that said, if SqlHydra had a way to generate the Primary Keys of each table, that would be immensely helpful for us. We have to manually write out our own PK selectors, and this is brittle and can introduce subtle bugs when a schema is updated. My other thought is that once you can get a PK from SqlHydra, I feel as if a lot of the functionality just boils down to:
Once you have the existing rows and incoming rows, the partitioning is just 5 lines of code. module Map =
let diff left right = Map.fold (fun state key _ -> Map.remove key state) left right
let existingRowMap = existingRows |> List.groupBy primaryKey<dbo.UserProjectFilters> |> Map
let incomingRowMap = incomingRows |> List.groupBy primaryKey<dbo.UserProjectFilters> |> Map
let rowsToAdd = Map.diff incomingRowMap existingRowMap |> Map.values
let rowsToRemove = Map.diff existingRowMap incomingRowMap |> Map.values
let rowsToChange = FSharpPlus.Map.intersect incomingRows existingRows |> Map.values I don't particularly mind if SqlHydra does that partitioning for you, but it seems a little out of scope to me to have the DB query library do that because it's just general collections processing and not very much code. Most of the rest of the boilerplate I think just comes from things like the whole match on
|
Beta Was this translation helpful? Give feedback.
-
Great feedback. Re: PK generation
The above Re: Helpers |
Beta Was this translation helpful? Give feedback.
-
I feel like the I think I will leave it, but also add a new overload of the |
Beta Was this translation helpful? Give feedback.
-
Hello @JordanMarr, I didn't use SqlHydra since sometimes so my knowledge is not so fresh on it. Not that I don't like it, I am trying to push it on the new project I am working on with a client 😉 With that said, indeed I remember having trouble with the I never had the case of need the diff helper that you presents in this discussion, but the exposed API seems to make sense. However, I am unsure how easy it will be to create a contract which is generic enough to cover most use cases. If this is the case, perhaps something that could be explored is to add it to the documentation and explains to people how it works so they can craft their own version of it? This the approach that was taken in Thoth.Json to cover specific use case which cannot be included in the core library as it is too specific / difficult to abstract. |
Beta Was this translation helpful? Give feedback.
-
The Idea
I'm interested in adding a layer of "save helpers" (for lack of a better name) on top of the query builders. Just a few light "ORM" style features to eliminate some of the boilerplate code of diffing and saving changes.
The Problem
There is too much boilerplate code required to partition a collection of records in insert, update, delete collections to be saved.
(This is driven by a user posted issue, and also my own projects.)
The Current Workflow
My boilerplate process to diff changes and save is as follows:
Prep:
Create a transaction from the QueryContext
Pull a list of existing records for that aggregate from DB.
Deletes:
Create a Set of PK values from incoming records
Use this to filter the existing records where PK value is not contained in the Set of incoming data (because it was deleted)
Create a delete query with the resulting idsToDelete
Upserts:
Put the existing records into a Map with the PK as the key
Create a for loop through incoming records
For each incoming record, lookup up the existing record from the map
Check for changes (either comparing the entire record for equality or just check individual columns)
If changes are detected, do an upsert. (Or, if you don't care to check for changes, just do an upsert and let PostgreSQL do the diffing for you.)
Cleanup:
Commit transaction
A Better Solution
In order to simplify this problem, I think the following would be a prerequisite:
I think this might look like a method that allows the save helper to obtain PK information that can be used to diff and partition the save collection.
I am open to ideas on dealing with this that range in complexity and simplicity. I'm posting this to see if any smart people can help brainstorm and architect a solution (as my brain needs a kickstart).
I specifically do not want to provide a stateful "Change Tracking" mechanism like EF or SQLProvider.)
Instead, I want something to help with the diff'ing.
The simplest solution would:
rowsToInsert
,rowsToUpdate
androwsToDelete
collections. The user would still be responsible for insert, updating and deleting. It would use the yet to be generated PK info to do so.(Or perhaps the user could just pass in one or more
getPK
functions instead of needing to generate that information into the types.)A more complex version might also:
@jwosty @MangelMaxime @MargaretKrutikova @ntwilson @Jmaharman @devinlyons @EverybodyKurts
Beta Was this translation helpful? Give feedback.
All reactions