Refactor Ecto transaction for efficiency using Ecto.Multi - ecto

I have a function that assigns multiple skills to a user. The skills already exist and have a many_to_many relationship with the user.
# skills will look like [%{id: "1"}, %{id: "4"}, %{id: "5"}] etc.
def reset(user, skills) do
user
|> Repo.preload(:skills)
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:skills, [])
|> Repo.update()
ids = Enum.map(skills, &Map.get(&1, :id))
query = Repo.all(from(p in Skill, where: p.id in ^ids))
skills = Enum.each(query, &Skill.create(user, &1))
end
Currently this works but feels inefficient:
We may be removing skills that will just be added back again
Might not need to run Repo.all to fetch skills, can just join them by ID if they exist
Could be wrapped in Ecto.Multi for database efficiency
In addition to this, it would be nice to return the created skills rather than just the :ok atom that Enum.each returns.
What would be the best way to refactor the code?

One improvement would be not assigning each skill one by one
def reset(user, skills) do
ids = Enum.map(skills, &Map.get(&1, :id))
skills = Repo.all(from(p in Question.Skill, where: p.id in ^ids))
user
|> Repo.preload(:skills)
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:skills, skills)
|> Repo.update()
end
If no change is needed (that is any deletion or addition to the user's skills), this makes two query to the database (assuming :skills hasn't been loaded, otherwise there'll be just one query, which is fetching the skills).
Also because we're making a changeset, it doesn't reset the whole thing. It only deletes or adds what is necessary.

Related

ArangoDB populate relation as field over graph query

I recently started using Arango since I want to make use of the advantages of graph databases. However, I'm not yet sure what's the most elegant and efficient approach to query an item from a document collection and applying fields to it that are part of a relation.
I'm used to make use of population or joins in SQL and NoSQL databases, but I'm not sure how it works here.
I created a document collection called posts. For example, this is a post:
{
"title": "Foo",
"content": "Bar"
}
And I also have a document collection called tags. A post can have any amount of tags, and my goal is to fetch either all or specific posts, but with their tags included, so for example this as my returning query result:
{
"title": "Foo",
"content": "Bar",
"tags": ["tag1", "tag2"]
}
I tried creating those two document collections and an edge collection post-tags-relation where I added an item for each tag from the post to the tag. I also created a graph, although I'm not yet sure what the vertex field is used for.
My query looked like this
FOR v, e, p IN 1..2 OUTBOUND 'posts/testPost' GRAPH post-tags-relation RETURN v
And it did give me the tag, but my goal is to fetch a post and include the tags in the same document...The path vertices do contain all tags and the post, but in separate arrays, which is not nice and easy to use (and probably not the right way). I'm probably missing something important here. Hopefully someone can help.
You're really close - it looks like your query to get the tags is correct. Now, just add a bit to return the source document:
FOR post IN posts
FILTER post._key == 'testPost'
LET tags = (
FOR v IN 1..2 OUTBOUND post
GRAPH post-tags-relation
RETURN v.value
)
RETURN MERGE(
post,
{ tags }
)
Or, if you want to skip the FOR/FILTER process:
LET post = DOCUMENT('posts/testPost')
LET tags = (
FOR v IN 1..2 OUTBOUND post
GRAPH post-tags-relation
RETURN v.value
)
RETURN MERGE(
post,
{ tags }
)
As for graph definition, there are three required fields:
edge definitions (an edge collection)
from collections (where your edges come from)
to collections (where your edges point to)
The non-obvious vertex collections field is there to allow you to include a set of vertex-only documents in your graph. When these documents are searched and how they're filtered remains a mystery to me. Personally, I've never used this feature (my data has always been connected) so I can't say when it would be valuable, but someone thought it was important to include.

Is there an efficient way for getting the rarest role of a member discord.js

I have implemented a feature that shows people their rarest role (the role that the least people have). I did it by looping through each of their roles and checking how many people have the role. As it turns out, if you have over like 50 roles, it delays the bots response visibly, so I was wondering if there is a more efficient way of doing the same thing. My code is here:
const rolesOfMember = member._roles
for(const role in rolesOfMember) {
var membersHavingCurrentRole = message.guild.roles.cache.get(rolesOfMember[role]).members.size
if(membersHavingCurrentRole < membersHavingRarestRole) {
membersHavingRarestRole = membersHavingCurrentRole
rarestRoleID = rolesOfMember[role]
} else if(membersHavingCurrentRole == membersHavingRarestRole) {
var rarestRole = message.guild.roles.cache.get(rarestRoleID)
var currentRole = message.guild.roles.cache.get(rolesOfMember[role])
if(rarestRole.comparePositionTo(currentRole) < 0) {
membersHavingRarestRole = membersHavingCurrentRole
rarestRoleID = rolesOfMember[role]
}
}
}
Well, one way to simplify your code would be to not have to call message.guild.roles.cache.get() 3 or so times for every role that the member has, especially if they're going to have 50+ roles. In fact, we could eliminate the need for your member._roles object entirely, and therefore eliminate the need for the for/in loop. Here's an example (tested and works):
var rolesOfMember = message.member.roles.cache;
var rarestRole = rolesOfMember.sort((roleA, roleB) => roleA.members.size - roleB.members.size).first();
var rarestRoleID = rarestRole.id;
In this example, I first get the Collection of all of the roles that the member has. Then, I use the Collection.sort() method to sort the roles based on how many members have each role in the guild (from least to greatest, so in other words from rarest to most common). Since the first role in this sorted Collection will be the rarest role that the user has, I simply call Collection.first() on the sorted Collection in order to retrieve the first and rarest Role. From there, you can use rarestRole however you'd like, I simply retrieve the role's ID and store it in a variable in my example.
You can also alter the sorting function however you'd like in order to consider the case in which two roles have the same amount of people who have them.
Relevant resources:
https://discord.js.org/#/docs/main/stable/class/Role?scrollTo=members
https://discord.js.org/#/docs/collection/master/class/Collection?scrollTo=sort

Many to many AQL query

I have 2 collections and one edge collection. USERS, FILES and FILES_USERS.
Im trying to get all FILES documents, that has the field "what" set to "video", for a specific user, but also embed another document, also from the collection FILES, but where the "what" is set to "trailer" and belongs to the "video" into the results.
I have tried the below code but its not working correctly, im getting a lot of duplicate results...its a mess. Im definitely doing it wrong.
FOR f IN files
FILTER f.what=="video"
LET trailer = (
FOR f2 IN files
FILTER f2.parent_key==f._key
AND f2.what=="trailer"
RETURN f2
)
FOR x IN files_users
FILTER x._from=="users/18418062"
AND x.owner==true
RETURN DISTINCT {f,trailer}
There may be a better way to do this with graph query syntax, but try this. Adjust the UNIQUE functions based on your data-model.
LET user_files = UNIQUE(FOR u IN FILES_USERS
FILTER u._from == "users/18418062" AND u.owner
RETURN u._to)
FOR uf IN user_files
FOR f IN files
FILTER f._key == uf AND f.what == "video"
LET trailers = UNIQUE(FOR t IN files
FILTER t.parent_key == f._key AND t.what == "trailer"
RETURN t)
RETURN {"video": f, "trailers": trailers}
Well, check to see If you have duplicate data as suggested by TMan, however check your query syntax too. It appears that you have no link between your f subquery and the x in the main query. That would cause the query to potentially return a lot of dups if there are multiple records in collection files_users for user users/18418062
Try adding a join in the main query. Something like:
FOR x IN files_users
FILTER x._from=="users/18418062"
AND x.owner==true
AND x._to == f._id
RETURN DISTINCT {f,trailer}
On a related note, if you run into performance issues doing a subquery for trailers , you could instead try just doing a join and array expansion and see if that works for your case

How to convert Rep[T] to T in slick 3.0?

I used a code, generated from slick code generator.
My table has more than 22 columns, hence it uses HList
It generates 1 type and 1 function:
type AccountRow
def AccountRow(uuid: java.util.UUID, providerid: String, email: Option[String], ...):AccountRow
How do I write compiled insert code from generated code?
I tried this:
val insertAccountQueryCompiled = {
def q(uuid:Rep[UUID], providerId:Rep[String], email:Rep[Option[String]], ...) = Account += AccountRow(uuid, providerId, email, ...)
Compiled(q _)
}
I need to convert Rep[T] to T for AccountRow function to work. How do I do that?
Thank you
;TLDR; Not possible
Explanation
There are two levels of abstraction in Slick: Querys and DBIOActions.
When you're dealing with Querys, you have to access your schema definitions, and rows, Reps and, basically, it's very constrained as it's the closest level of abstraction to the actual DB you're using. A Rep refers to an hypothetical value in the database, not in your program.
Then you have DBIOActions, which are the next level... not just some definition of a query, but the execution of it. You usually get DBIOActions when getting information out of a query, like with the result method or (TADAN!) when inserting rows.
Inserts and Updates are not queries and so what you're trying to do is not possible. You're dealing with DBIOAction (the += method), and Query stuff (the Rep types). The only way to get a Rep inside a DBIOAction is by executing a Query and obtaining a DBIOAction and then composing both Actions using flatMap or for comprehensions (which is the same).

How to query this with ORM?

I've using Kohana for a couple of weeks. One thing I noticed is that Kohana is missing eager loading (as far as I know). Let's say I have the following tables.
Subjects
id
name
Chapters
id
subject_id
name
Videos
id
chapter_id
name
When a user opens a subject page, I want to display all the chapters and videos. With ORM, I can do
$tutorials = ORM::factory('subject')->where('id','=', 1)->find();
foreach($tutorials as $tutorial)
{
$chapters = $tutorial->chapters->find_all();
foreach($chapters as $chapter)
{
$videos = $chapter->videos->find_all();
}
}
The above code is not efficient since it makes too many queries.
I thought about using join or database query builder, but both of them do not return a model object as their results. I also looked into with(), but it seems like it only works with one-to-one relationship.
using join on an ORM object returns an OPM object, but it doesn't return the data from the joining tables.
What would be my best option here? I would like to minimize # of queries and also want to get ORM objects a result. Whatever it would be, should return all the columns from tutorials, chapters, and videos.
First of all, your code is excess. ORM method find() returns 1 Model_Subject object. See
$chapters = ORM::factory('subject', 1)->chapters->find_all();
foreach($chapters as $chapter)
{
$videos = $chapter->videos->find_all();
}
With DB builder you can make just 2 requests. First get array of all chapters ids:
$chapters = DB::select('id')
->from('chapters')
->where('subject_id', '=', '1')
->execute()
->as_array(NULL, 'id');
Second - get all videos by ids as Model_Video object
$videos = DB::select('id')
->from('videos')
->where('chapter_id', 'IN', $chapters)
->as_object('Model_Video')
->execute()
->as_array();
So I guess you want something like this.
$videos = ORM::factory('Video')
->join(array('chapters', 'chapter'), 'LEFT')->on('video.chapter_id', '=', 'chapter.id')
->join(array('subjects', 'subject'), 'LEFT')->on('chapter.subject_id', '=', 'subject.id')
->where('subject.id', '=', $id)
->find_all();
Come to think of it, if the video belongs_to chapter belongs_to subject, try the following using with():
$videos = ORM::factory('Video')
->with('chapter:subject') // These are the names of the relationships. `:` is separator
// equals $video->chapter->subject;
->where('subject.id', '=', $id)
->find_all();
With things like this it often helps to think 'backwards'. You need the videos on that subject so start with the videos instead of the subject. :)
EDIT: The drawback of the second function is that it is going to preload all the data, it might be shorter to write but heavier on the server. I'd use the first one unless I need to know the subject and chapter anyway.

Resources