Reduce Remote Database Access

To implement caching in our web app we need to create a variables in the frontend to store our data variables. These data will need to be accessed by multiple frontend modules. Best practice for storing cached data is in it’s own module. So that’s what we will do.

Create Data Access Module

We have previously made a server module, but this module will be made client-side, since the data needs to be stored in the frontend. To be precise, it will be stored in the user’s browser.

  1. In the files menu

  2. Click on the Client Code ellipses

  3. Then choose Add Module

  4. Name the module data_access

create module

  1. Clean up the module by deleting everything except the imports.

delete

Cached User

We can now cache the user data. The idea is the front end will ask this module for the user data. If the data is saved in the variable, this module will return it. If there is no data in the variable, this module will retrieve it from the database, store the data for later user and then return the user data to the frontend.

First we need to create a variable to store the user data.

  1. copy the code below into the data_access module.

7# cached values
8__user = None

Code explaination

  • line 7 → a comment to help structure the code

  • line 8 → creates the variable we will use to store the user data

Dunder variables

In Python, we use double underscores before a variable (like __variable) for variables that can meant to be only used by the module they are in. This is called making them variable private to the module it’s in.

Doing this means the variable cannot be easily accessed or changed from outside the module. This is important because it makes sure that important parts of your code can’t be accidentally messed up.

Now we need to make a function that the frontend will use instead of anvil.users.get_user().

  1. Add the highlighted code to the bottom of the data_access module.

10def the_user():
11  global __user
12
13  if __user:
14    print("Using cached user")
15    return __user
16
17  print("Accessing user from database")
18  __user = anvil.users.get_user()
19  return __user

Code explaination

  • line 10 → creates the function the_user

  • line 11 → makes the __user global, which allows this function to edit it

  • line 13 → checks to see if there is any data stored in __user. Note __user starts with the value None which is False

  • line 14 → lets us know when the program is using cached user data

  • line 15 → returns the user data to the frontend (this will end the function if there is user data)

  • line 17 → lets us know when the program is retrieving user data from the backend

  • line 18 → gets current user data and stores it in the __user variable

  • line 19 → returns the user data to the frontend

We now have our caching user function, next we have to add it to our code

Refactor Code

Remember when we created the switch_component and we searched for code and replaced it. We will use that process now.

  1. Click on the search icon in the side menu

  2. Search for anvil.users.get_user()

search

You can see that five components have a call to anvil.users.get_user(). lets work through those from top to bottom.

AccountComponent

First component on our list is the AccountComponent.

  1. Open the AccountComponent in Code mode

  2. In the import section add the following highlighted line

1from ._anvil_designer import AccountComponentTemplate
2from anvil import *
3import anvil.server
4import anvil.tables as tables
5import anvil.tables.query as q
6from anvil.tables import app_tables
7import anvil.users
8from .. import data_access

Code explaination

  • line 8 → allows AccountComponent to use the data_access module

We need to get rid of the anvil.users.get_user() in line 17

  1. In the __init__ change line 17 to the highlighted code below

12  def __init__(self, **properties):
13    # Set Form properties and Data Bindings.
14    self.init_components(**properties)
15
16    # Any code you write here will run before the form opens.
17    user = data_access.the_user()
18    self.label_first_name.text = user["first_name"]
19    self.label_last_name.text = user["last_name"]

Code explaination

  • line 17 → uses our new the_user function to retrieve user data

Check that the AccountComponent is no longer in the search results, and then move onto the next one.

AddComponent

Next in our search results is the AddComponent

  1. Open AddComponent in Code mode.

  2. Add data_access to the import section

1from ._anvil_designer import AddComponentTemplate
2from anvil import *
3import anvil.server
4import anvil.tables as tables
5import anvil.tables.query as q
6from anvil.tables import app_tables
7import anvil.users
8from .. import data_access

Code explaination

  • line 8 → allows AccountComponent to use the data_access module

We need to get rid of the anvil.users.get_user() in line 22

  1. In the __init__ change line 22 to the highlighted code below

12  def __init__(self, **properties):
13    # Set Form properties and Data Bindings.
14    self.init_components(**properties)
15    self.subject = ""
16    self.details = ""
17    self.start = None
18    self.due = None
19
20    # Any code you write here will run before the form opens.
21    self.label_message.visible = False
22    if data_access.the_user():
23      self.card_details.visible = True
24      self.card_error.visible = False
25      self.button_add.visible = True
26    else:
27      self.card_details.visible = False
28      self.card_error.visible = True
29      self.button_add.visible = False

Code explaination

  • line 22 → uses our new the_user function to retrieve user data

Check the AddComponent is no longer in your search results, then onto the next one.

CalendarComponent

The next module we need to change is the CalendarComponent.

  1. Open CalendarComponent in Code mode.

  2. Add data_access to the import section

1from ._anvil_designer import CalendarComponentTemplate
2from anvil import *
3import plotly.graph_objects as go
4import anvil.server
5import anvil.tables as tables
6import anvil.tables.query as q
7from anvil.tables import app_tables
8import anvil.users
9from .. import data_access

Code explaination

  • line 9 → allows AccountComponent to use the data_access module

Once again we need to remove of the anvil.users.get_user() from the __init__

  1. In the __init__ change line 18 to the highlighted code below

13  def __init__(self, **properties):
14    # Set Form properties and Data Bindings.
15    self.init_components(**properties)
16
17    # Any code you write here will run before the form opens.
18    if anvil.users.get_user():
19      self.card_details.visible = True
20      self.card_error.visible = False
21      self.load_chart()
22    else:
23      self.card_details.visible = False
24      self.card_error.visible = True

Code explaination

  • line 18 → uses our new the_user function to retrieve user data

If CalendarComponent is no longer in your search results time to move on.

SetDetailsComponent

I know that MainForm is next in our search results, but we’ll come back to that, since SetDetailsComponent is pretty much the same as the all the other components.

  1. Open SetDetailsComponent in Code mode.

  2. Add data_access to the import section

1from ._anvil_designer import SetDetailsComponentTemplate
2from anvil import *
3import anvil.server
4import anvil.tables as tables
5import anvil.tables.query as q
6from anvil.tables import app_tables
7import anvil.users
8from .. import data_access

Code explaination

  • line 8 → allows AccountComponent to use the data_access module

Remove of the anvil.users.get_user() from the __init__

  1. In the __init__ change line 17 to the highlighted code below

12  def __init__(self, **properties):
13    # Set Form properties and Data Bindings.
14    self.init_components(**properties)
15
16    # Any code you write here will run before the form opens.
17    user = data_access.the_user()
18    if user["first_name"]:
19      self.text_box_first_name.text = user["first_name"]
20    if user["last_name"]:
21      self.text_box_last_name.text = user["last_name"]

Code explaination

  • line 17 → uses our new the_user function to retrieve user data

Now, the only form in our search should be MainForm, so time to move onto that.

MainForm

In the MainForm we have a number of instances of anvil.users.get_user(). We will have to replace each one, but before we do, we need to import data_access

  1. Open MainForm in Code mode

  2. Add the highlighted code to the end of the import section

 1from ._anvil_designer import MainFormTemplate
 2from anvil import *
 3import anvil.server
 4import anvil.tables as tables
 5import anvil.tables.query as q
 6from anvil.tables import app_tables
 7import anvil.users
 8from ..HomeComponent import HomeComponent
 9from ..CalendarComponent import CalendarComponent
10from ..AddComponent import AddComponent
11from ..AccountComponent import AccountComponent
12from ..SetDetailsComponent import SetDetailsComponent
13from ..WelcomeComponent import WelcomeComponent
14from .. import data_access

Code explaination

  • line 14 → allows AccountComponent to use the data_access module

Our first call to anvil.users.get_user() occurs in the switch_component method.

  1. Change the highlighted code in the switch_component method.

26  def switch_component(self, state):
27    # set state
28    if state == "home":
29      if data_access.the_user():
30        cmpt = HomeComponent()
31      else:
32        cmpt = WelcomeComponent()
33      breadcrumb = self.breadcrumb_stem

Code explaination

  • line 29 → uses our new the_user function to retrieve user data

The remaining four calls to anvil.users.get_user() are in the set_active_link method.

  1. Change the four highlighted lines below:

53  def set_active_link(self, state):
54    if state == "home":
55      self.link_home.role = "selected"
56    else:
57      self.link_home.role = None
58    if state == "add":
59      self.link_add.role = "selected"
60    else:
61      self.link_add.role = None
62    if state == "calendar":
63      self.link_calendar.role = "selected"
64    else:
65      self.link_calendar.role = None
66
67    self.link_register.visible = not data_access.the_user()
68    self.link_login.visible = not data_access.the_user()
69    self.link_account.visible = data_access.the_user()
70    self.link_logout.visible = data_access.the_user()

Code explaination

  • lines 67 - 70 → use our new the_user function to retrieve user data

Now, the only modules in our search results should be data_access and server modules.

Time to test.

Testing

Launch your web app and navigate to all the pages we change:

  • Account Page

  • Add Page

  • Calendar Page

  • Set Details Page

  • Home Page

  • Logout

Are things notably faster?

Problems

You will notice that there are some problems with:

  • Logging out

  • Updating user details

You will also notice that pages that load assessment data are still slow.

We will fix these problems in the next three tutorials.

Final code state

By the end of this tutorial your code should be the same as below:

Final MainForm

 1from ._anvil_designer import MainFormTemplate
 2from anvil import *
 3import anvil.server
 4import anvil.tables as tables
 5import anvil.tables.query as q
 6from anvil.tables import app_tables
 7import anvil.users
 8from ..HomeComponent import HomeComponent
 9from ..CalendarComponent import CalendarComponent
10from ..AddComponent import AddComponent
11from ..AccountComponent import AccountComponent
12from ..SetDetailsComponent import SetDetailsComponent
13from ..WelcomeComponent import WelcomeComponent
14from .. import data_access
15
16
17class MainForm(MainFormTemplate):
18  def __init__(self, **properties):
19    # Set Form properties and Data Bindings.
20    self.init_components(**properties)
21    self.breadcrumb_stem = self.label_title.text
22
23    # Any code you write here will run before the form opens.
24    self.switch_component("home")
25
26  def switch_component(self, state):
27    # set state
28    if state == "home":
29      if data_access.the_user():
30        cmpt = HomeComponent()
31      else:
32        cmpt = WelcomeComponent()
33      breadcrumb = self.breadcrumb_stem
34    elif state == "account":
35      cmpt = AccountComponent()
36      breadcrumb = self.breadcrumb_stem + " - Account"
37    elif state == "add":
38      cmpt = AddComponent()
39      breadcrumb = self.breadcrumb_stem + " - Add"
40    elif state == "calendar":
41      cmpt = CalendarComponent()
42      breadcrumb = self.breadcrumb_stem + " - Calendar"
43    elif state == "details":
44      cmpt = SetDetailsComponent()
45      breadcrumb = self.breadcrumb_stem + " - Account - Set Details"
46    
47    # execution
48    self.content_panel.clear()
49    self.content_panel.add_component(cmpt)
50    self.label_title.text = breadcrumb
51    self.set_active_link(state)
52  
53  def set_active_link(self, state):
54    if state == "home":
55      self.link_home.role = "selected"
56    else:
57      self.link_home.role = None
58    if state == "add":
59      self.link_add.role = "selected"
60    else:
61      self.link_add.role = None
62    if state == "calendar":
63      self.link_calendar.role = "selected"
64    else:
65      self.link_calendar.role = None
66
67    self.link_register.visible = not data_access.the_user()
68    self.link_login.visible = not data_access.the_user()
69    self.link_account.visible = data_access.the_user()
70    self.link_logout.visible = data_access.the_user()
71  
72  # --- link handlers
73  def link_home_click(self, **event_args):
74    self.switch_component("home")
75
76  def link_calendar_click(self, **event_args):
77    self.switch_component("calendar")
78
79  def link_add_click(self, **event_args):
80    self.switch_component("add")
81
82  def link_account_click(self, **event_args):
83    """This method is called when the link is clicked"""
84    self.switch_component("account")
85
86  def link_register_click(self, **event_args):
87    anvil.users.signup_with_form(allow_cancel=True)
88    self.switch_component("details")
89
90  def link_login_click(self, **event_args):
91    anvil.users.login_with_form(allow_cancel=True)
92    self.switch_component("home")
93
94  def link_logout_click(self, **event_args):
95    anvil.users.logout()
96    self.switch_component("home")

Final AccountComponent

 1from ._anvil_designer import AccountComponentTemplate
 2from anvil import *
 3import anvil.server
 4import anvil.tables as tables
 5import anvil.tables.query as q
 6from anvil.tables import app_tables
 7import anvil.users
 8from .. import data_access
 9
10
11class AccountComponent(AccountComponentTemplate):
12  def __init__(self, **properties):
13    # Set Form properties and Data Bindings.
14    self.init_components(**properties)
15
16    # Any code you write here will run before the form opens.
17    user = data_access.the_user()
18    self.label_first_name.text = user["first_name"]
19    self.label_last_name.text = user["last_name"]
20
21  def button_edit_click(self, **event_args):
22    main_form = get_open_form()
23    main_form.switch_component("details")

Final AddComponent

 1from ._anvil_designer import AddComponentTemplate
 2from anvil import *
 3import anvil.server
 4import anvil.tables as tables
 5import anvil.tables.query as q
 6from anvil.tables import app_tables
 7import anvil.users
 8from .. import data_access
 9
10
11class AddComponent(AddComponentTemplate):
12  def __init__(self, **properties):
13    # Set Form properties and Data Bindings.
14    self.init_components(**properties)
15    self.subject = ""
16    self.details = ""
17    self.start = None
18    self.due = None
19
20    # Any code you write here will run before the form opens.
21    self.label_message.visible = False
22    if data_access.the_user():
23      self.card_details.visible = True
24      self.card_error.visible = False
25      self.button_add.visible = True
26    else:
27      self.card_details.visible = False
28      self.card_error.visible = True
29      self.button_add.visible = False
30
31  def button_add_click(self, **event_args):
32    # validation
33    if not self.text_box_subject.text:
34      self.display_error("Subject name needed")
35    elif not self.text_box_details.text:
36      self.display_error("Assessment details needed")
37    elif not self.date_picker_start.date:
38      self.display_error("Start date needed")
39    elif not self.date_picker_due.date:
40      self.display_error("Due date needed")
41    else:
42      self.subject = self.text_box_subject.text
43      self.details = self.text_box_details.text
44      self.start = self.date_picker_start.date
45      self.due = self.date_picker_due.date
46      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
47      anvil.server.call('add_assessment', self.subject, self.details, self.start, self.due)
48      self.reset_form()
49
50  def display_error(self, message):
51    self.label_message.visible = True
52    self.label_message.foreground = "#ff0000"
53    self.label_message.icon = "fa:exclamation-triangle"
54    self.label_message.bold = True
55    self.label_message.text = message
56
57  def display_save(self, message):
58    self.label_message.visible = True
59    self.label_message.foreground = "#000000"
60    self.label_message.icon = "fa:save"
61    self.label_message.bold = False
62    self.label_message.text = message
63
64  def reset_form(self):
65    self.subject = ""
66    self.details = ""
67    self.start = None
68    self.due = None
69    self.text_box_subject.text = ""
70    self.text_box_details.text = ""
71    self.date_picker_start.date = None
72    self.date_picker_due.date = None

Final CalendarComponent

 1from ._anvil_designer import CalendarComponentTemplate
 2from anvil import *
 3import plotly.graph_objects as go
 4import anvil.server
 5import anvil.tables as tables
 6import anvil.tables.query as q
 7from anvil.tables import app_tables
 8import anvil.users
 9from .. import data_access
10
11
12class CalendarComponent(CalendarComponentTemplate):
13  def __init__(self, **properties):
14    # Set Form properties and Data Bindings.
15    self.init_components(**properties)
16
17    # Any code you write here will run before the form opens.
18    if data_access.the_user():
19      self.card_details.visible = True
20      self.card_error.visible = False
21      self.load_chart()
22    else:
23      self.card_details.visible = False
24      self.card_error.visible = True
25
26  def load_chart(self):
27    fig = anvil.server.call('get_chart')
28        
29    # Assign the Plotly figure to the Anvil Plot component
30    self.plot_timeline.figure = fig

Final SetDetailsComponent

 1from ._anvil_designer import SetDetailsComponentTemplate
 2from anvil import *
 3import anvil.server
 4import anvil.tables as tables
 5import anvil.tables.query as q
 6from anvil.tables import app_tables
 7import anvil.users
 8from .. import data_access
 9
10
11class SetDetailsComponent(SetDetailsComponentTemplate):
12  def __init__(self, **properties):
13    # Set Form properties and Data Bindings.
14    self.init_components(**properties)
15
16    # Any code you write here will run before the form opens.
17    user = data_access.the_user()
18    if user["first_name"]:
19      self.text_box_first_name.text = user["first_name"]
20    if user["last_name"]:
21      self.text_box_last_name.text = user["last_name"]
22
23  def button_save_click(self, **event_args):
24    
25    if self.text_box_first_name.text == "":
26      self.label_error.text = "First name cannot be blank"
27      self.label_error.visible = True
28      return
29
30    if self.text_box_last_name.text == "":
31      self.label_error.text = "Last name cannot be blank"
32      self.label_error.visible = True
33      return
34
35    self.label_error.visible = False
36    anvil.server.call("update_user", 
37                      self.text_box_first_name.text, 
38                      self.text_box_last_name.text)
39
40    main_form = get_open_form()
41    main_form.switch_component("account")