Skip to content

Latest commit

 

History

History
732 lines (585 loc) · 21.8 KB

31_remote_database.md

File metadata and controls

732 lines (585 loc) · 21.8 KB

Reduce Remote Database Access

- Learn how to create a module in the frontend to store cached data, specifically user data, and ensure it's only accessed by that module using Python's private variables.
- Learn to create a function that first checks if user data is cached; if not, it retrieves the data from the database and caches it for future use.
- Learn to modify existing components (AccountComponent, AddComponent, CalendarComponent, SetDetailsComponent, MainForm) to use the cached user data instead of repeatedly querying the database.
- Learn to test the web app to ensure the changes improve performance, and identify issues with logging out, updating user details, and page load speed.

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.
:linenos:
:lineno-start: 7
:emphasize-lines: 1-2
# cached values
__user = None
:class: notice
- **line 7** → a comment to help structure the code
- **line 8** → creates the variable we will use to store the user data
:class: note
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.
:linenos:
:lineno-start: 10
:emphasize-lines: 1-10
def the_user():
  global __user

  if __user:
    print("Using cached user")
    return __user

  print("Accessing user from database")
  __user = anvil.users.get_user()
  return __user
:class: notice
- **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
:linenos:
:lineno-start: 1
:emphasize-lines: 8
from ._anvil_designer import AccountComponentTemplate
from anvil import *
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from .. import data_access
:class: notice
- **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
:linenos:
:lineno-start: 12
:emphasize-lines: 6
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)

    # Any code you write here will run before the form opens.
    user = data_access.the_user()
    self.label_first_name.text = user["first_name"]
    self.label_last_name.text = user["last_name"]
:class: notice
- **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
:linenos:
:lineno-start: 1
:emphasize-lines: 8
from ._anvil_designer import AddComponentTemplate
from anvil import *
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from .. import data_access
:class: notice
- **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
:linenos:
:lineno-start: 12
:emphasize-lines: 11
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)
    self.subject = ""
    self.details = ""
    self.start = None
    self.due = None

    # Any code you write here will run before the form opens.
    self.label_message.visible = False
    if data_access.the_user():
      self.card_details.visible = True
      self.card_error.visible = False
      self.button_add.visible = True
    else:
      self.card_details.visible = False
      self.card_error.visible = True
      self.button_add.visible = False
:class: notice
- **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
:linenos:
:lineno-start: 1
:emphasize-lines: 9
from ._anvil_designer import CalendarComponentTemplate
from anvil import *
import plotly.graph_objects as go
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from .. import data_access
:class: notice
- **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
:linenos:
:lineno-start: 13
:emphasize-lines: 6
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)

    # Any code you write here will run before the form opens.
    if data_access.the_user():
      self.card_details.visible = True
      self.card_error.visible = False
      self.load_chart()
    else:
      self.card_details.visible = False
      self.card_error.visible = True
:class: notice
- **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
:linenos:
:lineno-start: 1
:emphasize-lines: 8
from ._anvil_designer import SetDetailsComponentTemplate
from anvil import *
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from .. import data_access
:class: notice
- **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
:linenos:
:lineno-start: 12
:emphasize-lines: 6
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)

    # Any code you write here will run before the form opens.
    user = data_access.the_user()
    if user["first_name"]:
      self.text_box_first_name.text = user["first_name"]
    if user["last_name"]:
      self.text_box_last_name.text = user["last_name"]
:class: notice
- **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
:linenos:
:lineno-start: 1
:emphasize-lines: 14
from ._anvil_designer import MainFormTemplate
from anvil import *
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from ..HomeComponent import HomeComponent
from ..CalendarComponent import CalendarComponent
from ..AddComponent import AddComponent
from ..AccountComponent import AccountComponent
from ..SetDetailsComponent import SetDetailsComponent
from ..WelcomeComponent import WelcomeComponent
from .. import data_access
:class: notice
- **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.
:linenos:
:lineno-start: 26
:emphasize-lines: 4
  def switch_component(self, state):
    # set state
    if state == "home":
      if data_access.the_user():
        cmpt = HomeComponent()
      else:
        cmpt = WelcomeComponent()
      breadcrumb = self.breadcrumb_stem
:class: notice
- **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:
:linenos:
:lineno-start: 53
:emphasize-lines: 15-18
  def set_active_link(self, state):
    if state == "home":
      self.link_home.role = "selected"
    else:
      self.link_home.role = None
    if state == "add":
      self.link_add.role = "selected"
    else:
      self.link_add.role = None
    if state == "calendar":
      self.link_calendar.role = "selected"
    else:
      self.link_calendar.role = None

    self.link_register.visible = not data_access.the_user()
    self.link_login.visible = not data_access.the_user()
    self.link_account.visible = data_access.the_user()
    self.link_logout.visible = data_access.the_user()
:class: notice
- **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

:linenos:
from ._anvil_designer import MainFormTemplate
from anvil import *
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from ..HomeComponent import HomeComponent
from ..CalendarComponent import CalendarComponent
from ..AddComponent import AddComponent
from ..AccountComponent import AccountComponent
from ..SetDetailsComponent import SetDetailsComponent
from ..WelcomeComponent import WelcomeComponent
from .. import data_access


class MainForm(MainFormTemplate):
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)
    self.breadcrumb_stem = self.label_title.text

    # Any code you write here will run before the form opens.
    self.switch_component("home")

  def switch_component(self, state):
    # set state
    if state == "home":
      if data_access.the_user():
        cmpt = HomeComponent()
      else:
        cmpt = WelcomeComponent()
      breadcrumb = self.breadcrumb_stem
    elif state == "account":
      cmpt = AccountComponent()
      breadcrumb = self.breadcrumb_stem + " - Account"
    elif state == "add":
      cmpt = AddComponent()
      breadcrumb = self.breadcrumb_stem + " - Add"
    elif state == "calendar":
      cmpt = CalendarComponent()
      breadcrumb = self.breadcrumb_stem + " - Calendar"
    elif state == "details":
      cmpt = SetDetailsComponent()
      breadcrumb = self.breadcrumb_stem + " - Account - Set Details"
    
    # execution
    self.content_panel.clear()
    self.content_panel.add_component(cmpt)
    self.label_title.text = breadcrumb
    self.set_active_link(state)
  
  def set_active_link(self, state):
    if state == "home":
      self.link_home.role = "selected"
    else:
      self.link_home.role = None
    if state == "add":
      self.link_add.role = "selected"
    else:
      self.link_add.role = None
    if state == "calendar":
      self.link_calendar.role = "selected"
    else:
      self.link_calendar.role = None

    self.link_register.visible = not data_access.the_user()
    self.link_login.visible = not data_access.the_user()
    self.link_account.visible = data_access.the_user()
    self.link_logout.visible = data_access.the_user()
  
  # --- link handlers
  def link_home_click(self, **event_args):
    self.switch_component("home")

  def link_calendar_click(self, **event_args):
    self.switch_component("calendar")

  def link_add_click(self, **event_args):
    self.switch_component("add")

  def link_account_click(self, **event_args):
    """This method is called when the link is clicked"""
    self.switch_component("account")

  def link_register_click(self, **event_args):
    anvil.users.signup_with_form(allow_cancel=True)
    self.switch_component("details")

  def link_login_click(self, **event_args):
    anvil.users.login_with_form(allow_cancel=True)
    self.switch_component("home")

  def link_logout_click(self, **event_args):
    anvil.users.logout()
    self.switch_component("home")

Final AccountComponent

:linenos:
from ._anvil_designer import AccountComponentTemplate
from anvil import *
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from .. import data_access


class AccountComponent(AccountComponentTemplate):
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)

    # Any code you write here will run before the form opens.
    user = data_access.the_user()
    self.label_first_name.text = user["first_name"]
    self.label_last_name.text = user["last_name"]

  def button_edit_click(self, **event_args):
    main_form = get_open_form()
    main_form.switch_component("details")

Final AddComponent

:linenos:
from ._anvil_designer import AddComponentTemplate
from anvil import *
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from .. import data_access


class AddComponent(AddComponentTemplate):
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)
    self.subject = ""
    self.details = ""
    self.start = None
    self.due = None

    # Any code you write here will run before the form opens.
    self.label_message.visible = False
    if data_access.the_user():
      self.card_details.visible = True
      self.card_error.visible = False
      self.button_add.visible = True
    else:
      self.card_details.visible = False
      self.card_error.visible = True
      self.button_add.visible = False

  def button_add_click(self, **event_args):
    # validation
    if not self.text_box_subject.text:
      self.display_error("Subject name needed")
    elif not self.text_box_details.text:
      self.display_error("Assessment details needed")
    elif not self.date_picker_start.date:
      self.display_error("Start date needed")
    elif not self.date_picker_due.date:
      self.display_error("Due date needed")
    else:
      self.subject = self.text_box_subject.text
      self.details = self.text_box_details.text
      self.start = self.date_picker_start.date
      self.due = self.date_picker_due.date
      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
      anvil.server.call('add_assessment', self.subject, self.details, self.start, self.due)
      self.reset_form()

  def display_error(self, message):
    self.label_message.visible = True
    self.label_message.foreground = "#ff0000"
    self.label_message.icon = "fa:exclamation-triangle"
    self.label_message.bold = True
    self.label_message.text = message

  def display_save(self, message):
    self.label_message.visible = True
    self.label_message.foreground = "#000000"
    self.label_message.icon = "fa:save"
    self.label_message.bold = False
    self.label_message.text = message

  def reset_form(self):
    self.subject = ""
    self.details = ""
    self.start = None
    self.due = None
    self.text_box_subject.text = ""
    self.text_box_details.text = ""
    self.date_picker_start.date = None
    self.date_picker_due.date = None

Final CalendarComponent

:linenos:
from ._anvil_designer import CalendarComponentTemplate
from anvil import *
import plotly.graph_objects as go
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from .. import data_access


class CalendarComponent(CalendarComponentTemplate):
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)

    # Any code you write here will run before the form opens.
    if data_access.the_user():
      self.card_details.visible = True
      self.card_error.visible = False
      self.load_chart()
    else:
      self.card_details.visible = False
      self.card_error.visible = True

  def load_chart(self):
    fig = anvil.server.call('get_chart')
        
    # Assign the Plotly figure to the Anvil Plot component
    self.plot_timeline.figure = fig

Final SetDetailsComponent

:linenos:
from ._anvil_designer import SetDetailsComponentTemplate
from anvil import *
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.users
from .. import data_access


class SetDetailsComponent(SetDetailsComponentTemplate):
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)

    # Any code you write here will run before the form opens.
    user = data_access.the_user()
    if user["first_name"]:
      self.text_box_first_name.text = user["first_name"]
    if user["last_name"]:
      self.text_box_last_name.text = user["last_name"]

  def button_save_click(self, **event_args):
    
    if self.text_box_first_name.text == "":
      self.label_error.text = "First name cannot be blank"
      self.label_error.visible = True
      return

    if self.text_box_last_name.text == "":
      self.label_error.text = "Last name cannot be blank"
      self.label_error.visible = True
      return

    self.label_error.visible = False
    anvil.server.call("update_user", 
                      self.text_box_first_name.text, 
                      self.text_box_last_name.text)

    main_form = get_open_form()
    main_form.switch_component("account")