Fauna Relationships

Hi there,

I’ve got a simple setup, pretty much like here with User and Car.

What I want to do is populate User.cars (an array of refs), when a new Car is Created, so that ultimately I can query an index and get all cars from a user or multiple users

Reading this it appears that I’d need to write a GraphQL mutation to connect the Car to the User.

From what I understand–and my understanding is extremely limited–about Fauna is that it takes GraphQL and somehow morphs that into FQL.

After reading this, which is also related to relationships, it seems that I can use FQL directly to create a Car, and add a ref to a User (in the cars field).

This seems like it would take two steps. First create the Car and then add the ref of the Car to the user’s “cars” array on the User.

What I’d like to do is create a Car and have it associated with a User in one step, using FQL only, if possible.

If not, it looks like using mutations is the way forward.

Any advice would be most appreciated; thanks.

You can perform multiple operations in a single query, including the creation of multiple documents.

If you need to apply a relation in such a query, you still need to create a document to capture its reference so that another document can contain the reference. The Let function is helpful for this situation, as introduced here.

For your example, where you have owners and cars and need a one-to-many relationship, it would make sense for the car documents to contain an ownedBy field that has a reference to the car’s owner document.

If you’re creating an owner and car document at once, your query would look like this:

Let(
  {
    owner: Create(
      Collection("owners"),
      {
        data: {
          name: "Bob"
          // additional owner information goes here
        }
      }
    ),
    car: Create(
      Collection("cars"),
      {
        data: {
          make: "Ford",
          ownedBy: Select("ref", Var("owner")),
          // additional car information goes here
        }
      }
    )
  },
  {
    owner: Var("owner"),
    car: Var("car"),
  }
)

All FQL functions return a result. When you call Create, it returns the document created. We can capture that result and then use Select to pull out the document’s reference.

When I execute that query, the result is:

{
  owner: {
    ref: Ref(Collection("owners"), "341620316727935488"),
    ts: 1662053372040000,
    data: { name: 'Bob' }
  },
  car: {
    ref: Ref(Collection("cars"), "341620316736324096"),
    ts: 1662053372040000,
    data: {
      make: 'Ford',
      ownedBy: Ref(Collection("owners"), "341620316727935488")
    }
  }
}

The second parameter to Let is the “result”, which I’ve fashioned as an object that contains both the owner and car documents. You can return any result you like, including using another r FQL function call, or something simple such as "Done" if you don’t need any details from the query.

1 Like

Thanks for that very helpful reply @ewan.

This got me thinking:

Even though this is a single query it’s still two write ops.

My original intent was to have all Users have an array of cars; however, you suggested to just give every Car an owner with is the User’s ref.

Essentially this is what I was already doing, but a bit more complication.

Let me explain:

I’m going to introduce the concept of a CarPool, which goes along with the car theme.

Each User has their own CarPool, which includes a list of Users (their ids).

When I want to get a list of all Cars in the CarPool, I have a query that does this:

q.Map(
    q.Filter(
        q.Paginate(
            // index on Cars w/ values ownerId & the ref w/o terms
            q.Match(q.Index("cars_by_owner_id")), 
            options
        ),
        q.Lambda((ownerId, ref) =>
            q.Or(
                q.Equals(ownerId, myId),
                // myCarPool is supplied as a variable here as an array of ownerIds
                q.ContainsValue(ownerId, myCarPool) 
            )
        )
    ),
    q.Lambda((ownerId, ref) => q.Get(ref))
)

I made it this way, because I didn’t understand how relationships worked in Fauna yet.

When I read the docs, it seemed to make sense to create some relationships, for example:

  1. Create a CarPool
  2. Add Users to CarPool.
  3. Add Cars to Users

Then query the CarPool to get the Users and their Cars.

While my initial quick-and-dirty approach seemed naive, I now think it might be more efficient and simpler without all the overhead.

What are your thoughts?

You haven’t defined “CarPool,” so I don’t know what you are trying to do. To me, a “carpool” is a group of friends/associates who take turns driving the group from the suburbs into the city in the morning and from the city in the evening. Does that sound like what you are trying to model?

Then query the CarPool to get the Users and their Cars.

Are you using CarPool documents to maintain many-to-many relations? If so,
step 3 should be “Add Cars to CarPool.”

While it is possible to store arrays in documents, array manipulation is a bit difficult because there isn’t a built-in slice(), so there is no simple way to update an entry at a specific index.

Also, Fauna creates one index entry per array item if you attempt to index an array field. If you try to index multiple array fields, the number of index entries created is the Cartesian product of each array’s number of index entries.

So, it is much more flexible and efficient to avoid arrays and manage references in either document of a one-to-many relation or a “join collection” for many-to-many relations.

Thanks again for your helpful reply, @ewan.

I think I stretched the analogy too far w/o proper explanation. Sorry about that.

In this case, a Carpool would be “pool of cars” that are available via their owners (i.e. users) who are added, as members, to a pool.

It might be easier if a Carpool was thought of as a car dealership. Users can be car lot owners, owning many cars. When a user is added to a Carpool, they become members of the dealership, and anyone who is a member of the dealership can see all cars of all members.

The use case would be: as a user I want to see all cars of all members in the car pool. And I want to do that w/o querying multiple collections. Further, when a car is created, it becomes visible through the relationship of car => user => carpool.

With your help in your initial response, I got that working and it got me thinking that the creation of these separate collections with relationships was more overhead and potentially more costly than a simpler solution, like having each car have fields (ownerId, carpoolId, etc.) that establish a relationship.

It’s still a bit fuzzy in my brain, so let me work on this and come back with my results after working on solutions using both approaches.

Hi @ewan,

I kinda came to the conclusion that creating relationships might be the best way forward for a certain use cases.

Moving away from the cars example in prior threads, what I’m trying to do with the following query is:

  1. Create a User
  2. Create a ChefNetwork, using info from the newly created User
  3. Update the newly created User with the with the id of the newly created ChefNetwork

All works up to the point where I’ve marked “need some help” in the below.

Let(
    {
        user: Create(Collection("User"), {
            data: { ...payload },
        }),
        cn: Create(Collection("ChefNetwork"), {
            data: {
                owner: Select("ref", Var("user")),
                ownerId: Select(["ref", "id"], Var("user")),
                ownerChefId: Select(
                    ["data", "chefId"],
                    Var("user")
                ),
                ownerChefName: Select(
                    ["data", "chefName"],
                    Var("user")
                ),
                ownerChefNickname: Select(
                    ["data", "chefNickname"],
                    Var("user")
                ),
                members: [
                    {
                        user: Select("ref", Var("user")),
                        chefName: Select(
                            ["data", "chefName"],
                            Var("user")
                        ),
                        chefNickname: Select(
                            ["data", "chefNickname"],
                            Var("user")
                        ),
                        isEditor: true,
                    },
                ],
            },
        }),
        updatedUser: Update(
            Ref(
                Collection("User"),
                Select(["ref", "id"], Var("user"))
            ),
            {
                data: {
                    ...Select("data", Var("user")), //<== need help here
                    chefNetworkId: Select(
                        ["ref", "id"],
                        Var("cn")
                    ),
                },
            }
        ),
    },
    Var("user")
)

I obviously don’t know what I’m doing, but I’ve tried a bunch of stuff. The short of it is that I want to update the user, but to do that I need the User data.

What I wind up with on the User is the below:

raw: {
    select: "data",
    from: {
      ref: Ref(Collection("User"), "34424219572476XXXX"),
      class: Collection("User"),
      collection: Collection("User"),
      ts: 1664553790700000,
      data: {
        chefId: "bb772ca6-ef7e-4f0d-9e25-87e2629e863a",
        chefName: "nancy",
        chefNickname: "nancy",
        chefNetwork: [
          {
            chefName: "nancy",
            chefNickname: "nancy",
            chefId: "bb772ca6-ef7e-4f0d-9e25-87e2629eXXX",
            isEditor: true
          }
        ],
        chefNetworks: [],
        counts: {
          recipes: 0,
          slurps: 0,
          chefsInNetwork: 1
        },
        role: "normie",
        secretCode: "NRZJf84te6XXXXX",
        createdAt: 1664553787430,
        updatedAt: 1664553787430,
        lastSignedInOn: 1664553787430
      }
    }
  },

The raw bit makes me think I need to do something more with that ...Select bit.

I can do a workaround, where I exclude the third entry of the query, and then run another query using the responses from the first, but I’d like to know how I can do the above without getting raw.

Any advice would be appreciated.

I see where you are going, but you haven’t really described what you are trying to do. For example, what does a ChefNetwork document represent?

Typically, in a many-to-many relationship, you don’t maintain those relationships in the documents that are being related.

For example, if you have a “team” of chefs that is represented by the ChefNetwork document, those documents would contain the details of the team itself, but would not contain any chef references. Your User collection seems to store chef details (think about renaming to Chef?). You would then create a ChefNetworkRelations collection with documents that only connect a User document with a ChefNetwork document.

It might help to visualize it like this:

+=User==========+      +=Relation=====+      +=ChefNetwork=+
| ref           |<--+  | ref          |  +-->| ref         |
| data: {       |   |  | data: {      |  |   | data: {     |
|  chefName     |   +--|   userRef    |  |   |   name      |
|  chefNickname |      |   networkRef |--+   |   owner     |
| ...           |      |   isEditor   |      | ...         |
+===============+      +==============+      +=============+

That kind of relationship makes it easy to find which chefs belong to a specific network, and which networks a chef might belong to. It also means that you’re not duplicating data from each member in the relation.

That last part is important for data integrity. Just think what happens when a chef wants to update his nickname. The way you have it, you’d have to change the User document, and at least two places in the ChefNetwork document. If you rely on the relations, you only need to change it once.

Without knowing the goal and where the constraints need to be applied, it’s hard to give any further solid advice.

But I can provide some suggestions based on what you’ve shown in your query:

In this part of your query:

        cn: Create(Collection("ChefNetwork"), {
            data: {
                owner: Select("ref", Var("user")),
                ownerId: Select(["ref", "id"], Var("user")),

Why are you storing the ownerId field? It’s part of the reference, so you’re just duplicating that part, and it means you’d have to reconstruct the reference every time you want to use the document ID to fetch the document. That can also make your relationships rigid as only documents in the User collection could be used.

In this part of your query:

        updatedUser: Update(
            Ref(
                Collection("User"),
                Select(["ref", "id"], Var("user"))
            ),

The user value already contains a reference, so you can simplify that to:

        updatedUser: Update(
            Select("ref", Var("user")),

In the “need help here” portion of the query, know that Update modifies the fields specified and leaves the others intact. There is no need to compose the entire document again. So you can just:

            {
                data: {
                    chefNetwork: Select("ref", Var("cn"))
                },
            }

Again, I’m not sure why you’re extracting the document ID from the references.

2 Likes